diff --git a/web/package.json b/web/package.json index 02436ee134..fa7d565e15 100644 --- a/web/package.json +++ b/web/package.json @@ -28,6 +28,7 @@ "@types/emscripten": "^1.39.6", "@types/jest": "^27.0.3", "@types/offscreencanvas": "^2019.6.4", + "@types/wechat-miniprogram": "^3.4.0", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^5.9.1", "esbuild": "^0.13.14", @@ -35,10 +36,12 @@ "eslint-config-alloy": "^4.4.0", "jest": "^26.6.3", "jest-canvas-mock": "^2.3.1", + "magic-string": "^0.25.7", "rimraf": "^3.0.2", "rollup": "^2.38.5", "rollup-plugin-esbuild": "^4.7.1", "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-jscc": "^2.0.0", "ts-jest": "^26.5.0", "tslib": "^2.1.0", "typedoc": "^0.22.10", diff --git a/web/script/plugin/replace-config.js b/web/script/plugin/replace-config.js new file mode 100644 index 0000000000..a287019b35 --- /dev/null +++ b/web/script/plugin/replace-config.js @@ -0,0 +1,255 @@ +/* eslint-disable */ +export const replaceFunctionConfig = [ + { + name: 'replace __emval_get_method_caller', + start: 'function __emval_get_method_caller(argCount, argTypes)', + end: 'function __emval_get_module_property(name)', + type: 'function', + replaceStr: (function __emval_get_method_caller(argCount, argTypes) { + var types = __emval_lookupTypes(argCount, argTypes); + var retType = types[0]; + var signatureName = retType.name + "_$" + types.slice(1).map(function(t) { + return t.name; + }).join("_") + "$"; + var returnId = emval_registeredMethods[signatureName]; + if (returnId !== void 0) { + return returnId; + } + var params = ["retType"]; + var args = [retType]; + var argsList = ""; + for (var i2 = 0; i2 < argCount - 1; ++i2) { + argsList += (i2 !== 0 ? ", " : "") + "arg" + i2; + params.push("argType" + i2); + args.push(types[1 + i2]); + } + var functionName = makeLegalFunctionName("methodCaller_" + signatureName); + var functionBody = "return function " + functionName + "(handle, name, destructors, args) {\n"; + var offset = 0; + for (var i2 = 0; i2 < argCount - 1; ++i2) { + functionBody += " var arg" + i2 + " = argType" + i2 + ".readValueFromPointer(args" + (offset ? "+" + offset : "") + ");\n"; + offset += types[i2 + 1]["argPackAdvance"]; + } + functionBody += " var rv = handle[name](" + argsList + ");\n"; + for (var i2 = 0; i2 < argCount - 1; ++i2) { + if (types[i2 + 1]["deleteObject"]) { + functionBody += " argType" + i2 + ".deleteObject(arg" + i2 + ");\n"; + } + } + if (!retType.isVoid) { + functionBody += " return retType.toWireType(destructors, rv);\n"; + } + functionBody += "};\n"; + params.push(functionBody); + var anonymous = function (retType) { + var parentargs = Array.from(arguments); + parentargs.shift(); + return this[makeLegalFunctionName("methodCaller_" + signatureName)] = function (handle, name, destructors, args) { + var paramList = []; + var offset = 0; + for (var i =0; i < parentargs.length; i ++){ + paramList.push(parentargs[i].readValueFromPointer(args + offset)); + offset += types[i + 1]["argPackAdvance"] + } + var rv = handle[name](...paramList); + if (!retType.isVoid) { + return retType.toWireType(destructors, rv); + } + } + } + var invokerFunction = anonymous.apply({}, args); + returnId = __emval_addMethodCaller(invokerFunction); + emval_registeredMethods[signatureName] = returnId; + return returnId; + }).toString() + }, + { + name: 'replace craftEmvalAllocator', + start: 'function craftEmvalAllocator(argCount)', + end: 'var emval_newers = {};', + type: 'function', + replaceStr: (function craftEmvalAllocator(argCount) { + var argsList = ""; + for (var i2 = 0; i2 < argCount; ++i2) { + argsList += (i2 !== 0 ? ", " : "") + "arg" + i2; + } + var functionBody = "return function emval_allocator_" + argCount + "(constructor, argTypes, args) {\n"; + for (var i2 = 0; i2 < argCount; ++i2) { + functionBody += "var argType" + i2 + " = requireRegisteredType(Module['HEAP32'][(argTypes >>> 2) + " + i2 + '], "parameter ' + i2 + '");\nvar arg' + i2 + " = argType" + i2 + ".readValueFromPointer(args);\nargs += argType" + i2 + "['argPackAdvance'];\n"; + } + functionBody += "var obj = new constructor(" + argsList + ");\nreturn valueToHandle(obj);\n}\n"; + function anonymous(requireRegisteredType, Module, valueToHandle) { + return function(constructor, argTypes, args) { + var resultList = []; + for (var i2 = 0; i2 < argCount; ++i2) { + var currentArg = requireRegisteredType(Module['HEAP32'][(argTypes >>> 2) + i2], `parameter ${i2}`); + var res = currentArg.readValueFromPointer(args); + resultList.push(res); + args += currentArg['argPackAdvance'] + } + var obj = new constructor(...resultList); + return valueToHandle(obj) + } + } + var invokerFunction = anonymous.apply({}, [requireRegisteredType, Module, Emval.toHandle]); + return invokerFunction + }).toString() + }, + { + name: 'replace craftInvokerFunction', + start: 'function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc)', + end: 'function heap32VectorToArray(count, firstElement)', + type: 'funtcion', + replaceStr: (function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc) { + var createOption = {}; + createOption.argCount = argTypes.length; + var argCount = argTypes.length; + if (argCount < 2) { + throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!"); + } + createOption.isClassMethodFunc = argTypes[1] !== null && classType !== null; + var isClassMethodFunc = argTypes[1] !== null && classType !== null; + var needsDestructorStack = false; + for (var i2 = 1; i2 < argTypes.length; ++i2) { + if (argTypes[i2] !== null && argTypes[i2].destructorFunction === void 0) { + needsDestructorStack = true; + break; + } + } + createOption.needsDestructorStack = needsDestructorStack; + var returns = argTypes[0].name !== "void"; + createOption.returns = argTypes[0].name !== "void"; + createOption.childArgs = []; + createOption.childDtorFunc = []; + for (var i2 = 0; i2 < argCount - 2; ++i2) { + createOption.childArgs.push(i2); + } + var args1 = ["throwBindingError", "invoker", "fn", "runDestructors", "retType", "classParam"]; + var args2 = [throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]]; + for (var i2 = 0; i2 < argCount - 2; ++i2) { + args1.push("argType" + i2); + args2.push(argTypes[i2 + 2]); + } + args2.push(Asyncify); + if (!needsDestructorStack) { + for (var i2 = isClassMethodFunc ? 1 : 2; i2 < argTypes.length; ++i2) { + var paramName = i2 === 1 ? "thisWired" : "arg" + (i2 - 2) + "Wired"; + if (argTypes[i2].destructorFunction !== null) { + createOption.childDtorFunc.push({ + paramName, + func: argTypes[i2].destructorFunction, + index: i2 + }); + } + } + } + function anonymous(throwBindingError, invoker, fn, runDestructors, retType, classParam) { + const anonymousArg = Array.from(arguments); + + return this[makeLegalFunctionName(humanName)] = function () { + var parentargs = Array.from(arguments); + var argumentLen = createOption.childArgs.length; + if (parentargs.length !== argCount - 2) { + throwBindingError('function _PAGPlayer._getComposition called with ' + parentargs.length + ' arguments, expected 0 args!'); + } + const argArr = anonymousArg.slice(6, 6 + argumentLen); + var destructors = []; + var dtorStack = needsDestructorStack ? destructors : null; + var thisWired; + var argWiredList = []; + var rv; + if (isClassMethodFunc) { + thisWired = classParam.toWireType(dtorStack, this) + } + for (var i = 0; i < createOption.childArgs.length; i ++) { + argWiredList.push(argArr[i].toWireType(dtorStack, parentargs[i])); + } + if(isClassMethodFunc) { + rv = invoker(fn, thisWired, ...argWiredList); + } else { + rv = invoker(fn, ...argWiredList); + } + function onDone(crv) { + if (needsDestructorStack) { + runDestructors(destructors) + } else { + const funcOption = createOption.childDtorFunc; + for (var i = 0; i < funcOption.length; i ++) { + const currentOption = funcOption[i]; + if (currentOption.index === 1) { + currentOption.func(thisWired); + } else { + const ci = createOption.childArgs.indexOf(currentOption.index); + if (ci >= 0) { + currentOption.func(argWiredList[ci]); + } + } + } + } + if (returns) { + var ret = retType.fromWireType(crv); + return ret; + } + } + return Asyncify.currData ? Asyncify.whenDone().then(onDone) : onDone(rv); + } + } + var invokerFunction = anonymous.apply({}, args2); + return invokerFunction; + }).toString() + }, + { + name: 'replace createNamedFunction', + start: 'function createNamedFunction(name, body)', + end: 'function extendError(baseErrorType, errorName)', + replaceStr: (function createNamedFunction(name, body) { + name = makeLegalFunctionName(name); + return function() { + return body.apply(this, arguments); + } + }).toString() + }, + { + name: 'replace getBinaryPromise', + start: 'function getBinaryPromise()', + end: 'function createWasm()', + replaceStr: (function getBinaryPromise() { + return new Promise((resolve, reject) => { + if (globalThis.isWxWebAssembly) { + resolve(wasmBinaryFile) + } else { + const fs = wx.getFileSystemManager() + fs.readFile({ + filePath: wasmBinaryFile, + position: 0, + success(res) { + resolve(res.data); + }, + fail(res) { + reject(res); + } + }); + } + }); + }).toString() + }, + { + name: 'replace WebAssembly Runtime error', + type: 'string', + start: 'var e = new WebAssembly.RuntimeError(what);', + replaceStr: 'var e = "run time error";' + }, + { + name: 'replace performance', + type: 'string', + start: 'performance.now();', + replaceStr: 'wx.getPerformance().now();' + }, + { + name: 'replace libpag.wasm name', + type: 'string', + start: 'libpag.wasm', + replaceStr: 'libpag.wasm.br' + }, +]; + diff --git a/web/script/plugin/rollup-plugin-replace.js b/web/script/plugin/rollup-plugin-replace.js new file mode 100644 index 0000000000..326a1639d7 --- /dev/null +++ b/web/script/plugin/rollup-plugin-replace.js @@ -0,0 +1,30 @@ +import MagicString from 'magic-string'; +import { replaceFunctionConfig } from './replace-config'; + +export default function replaceFunc() { + return { + name: 'replaceFunc', + transform (code, id) { + let codeStr = `${code}`; + const magic = new MagicString(codeStr); + replaceFunctionConfig.forEach(item => { + if (item.type === 'string') { + const startOffset = codeStr.indexOf(item.start); + if (startOffset > -1) { + magic.overwrite(startOffset, startOffset + item.start.length, item.replaceStr) + } + } else { + const startOffset = codeStr.indexOf(item.start); + const endOffset = codeStr.indexOf(item.end); + if (startOffset > -1 && endOffset > startOffset && item.replaceStr) { + magic.overwrite(startOffset, endOffset, item.replaceStr) + } + } + }) + return { + code: magic.toString(), + map: magic.generateMap({ hires: true }) + } + } + } +} diff --git a/web/script/rollup.config.js b/web/script/rollup.config.js index d80459b03d..338c118e0a 100644 --- a/web/script/rollup.config.js +++ b/web/script/rollup.config.js @@ -3,6 +3,8 @@ import commonJs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; import { terser } from 'rollup-plugin-terser'; import esbuild from 'rollup-plugin-esbuild'; +import jscc from 'rollup-plugin-jscc'; +import replaceFunc from './plugin/rollup-plugin-replace'; import pkg from '../package.json'; @@ -55,6 +57,7 @@ const umdMinConfig = { plugins: [esbuild({ tsconfig: 'tsconfig.json', minify: false }), json(), resolve(), commonJs(), terser()], }; + export default [ umdConfig, umdMinConfig, @@ -64,6 +67,13 @@ export default [ { banner, file: pkg.module, format: 'esm', sourcemap: true }, { banner, file: pkg.main, format: 'cjs', exports: 'auto', sourcemap: true }, ], - plugins: [esbuild({ tsconfig: 'tsconfig.json', minify: false }), resolve(), commonJs()], + plugins: [esbuild({ tsconfig: 'tsconfig.json', minify: false }), resolve(), commonJs() ], + }, + { + input: 'src/pag.ts', + output: [ + { banner, file: 'lib/libpag.wx.js', format: 'cjs', exports: 'auto', sourcemap: true }, + ], + plugins: [jscc({ values: { _WECHAT: 1 } }), esbuild({ tsconfig: 'tsconfig.json', minify: false }), resolve(), commonJs(), replaceFunc()], }, ]; diff --git a/web/src/binding.ts b/web/src/binding.ts index e5e06e3f38..aadf0791b0 100644 --- a/web/src/binding.ts +++ b/web/src/binding.ts @@ -7,10 +7,14 @@ import { PAGView } from './pag-view'; import { PAGFont } from './pag-font'; import { PAGLayer } from './pag-layer'; import { PAGComposition } from './pag-composition'; -import { VideoReader } from './core/video-reader'; import { ScalerContext } from './core/scaler-context'; import { WebMask } from './core/web-mask'; import { NativeImage } from './core/native-image'; +/* #if _WECHAT +import { VideoReader } from './core/wechat-video-reader'; +//#else */ +import { VideoReader } from './core/video-reader'; +// #endif /** * Binding pag js module on pag webassembly module. @@ -54,14 +58,7 @@ export const binding = (module: PAG) => { return new PAGSurface(module, pagSurfaceWasm); }; module._PAGSurface.FromFrameBuffer = async function (frameBufferID, width, height, flipY): Promise { - const pagSurfaceWasm = await module.webAssemblyQueue.exec( - this._FromFrameBuffer, - this, - frameBufferID, - width, - height, - flipY, - ); + const pagSurfaceWasm = await module.webAssemblyQueue.exec(this._FromFrameBuffer, this, frameBufferID, width, height, flipY); return new PAGSurface(module, pagSurfaceWasm); }; module._PAGImage.FromBytes = async function (bytes, length): Promise { diff --git a/web/src/core/scaler-context.ts b/web/src/core/scaler-context.ts index fe2bea6fc2..63a4c4f4aa 100644 --- a/web/src/core/scaler-context.ts +++ b/web/src/core/scaler-context.ts @@ -1,5 +1,9 @@ import { NativeImage } from './native-image'; import { measureText } from '../utils/measure-text'; +/* #if _WECHAT +import { wxOffscreenManager } from '../utils/offscreen-canvas-manager' +//#else */ +// #endif export interface Bounds { top: number; @@ -8,6 +12,10 @@ export interface Bounds { right: number; } +/* #if _WECHAT +const wxFreeNode = wxOffscreenManager.getFreeCanvas(); +const canvas = wxFreeNode.canvas; +//#else */ const canvas = ((): HTMLCanvasElement | OffscreenCanvas => { try { const offscreenCanvas = new OffscreenCanvas(0, 0); @@ -18,10 +26,18 @@ const canvas = ((): HTMLCanvasElement | OffscreenCanvas => { return document.createElement('canvas'); } })(); +// #endif + canvas.width = 10; canvas.height = 10; +/* #if _WECHAT +const wxFreeNodeTest = wxOffscreenManager.getFreeCanvas(); +const testCanvas = wxFreeNodeTest.canvas; +//#else */ const testCanvas = document.createElement('canvas'); +// #endif + testCanvas.width = 1; testCanvas.height = 1; const testContext = testCanvas.getContext('2d'); diff --git a/web/src/core/web-mask.ts b/web/src/core/web-mask.ts index 0db8ffc715..6fefc205f7 100644 --- a/web/src/core/web-mask.ts +++ b/web/src/core/web-mask.ts @@ -1,5 +1,8 @@ import { ScalerContext } from './scaler-context'; - +/* #if _WECHAT +import { wxOffscreenManager } from '../utils/offscreen-canvas-manager' +//#else */ +// #endif export class WebMask { public static module; @@ -25,10 +28,15 @@ export class WebMask { } } - private readonly canvas: HTMLCanvasElement; + private readonly canvas: HTMLCanvasElement | OffscreenCanvas; public constructor(width: number, height: number) { + /* #if _WECHAT + this.wxFreeNode = wxOffscreenManager.getFreeCanvas(); + this.canvas = this.wxFreeNode.canvas; + //#else */ this.canvas = document.createElement('canvas'); + // #endif this.canvas.width = width; this.canvas.height = height; } @@ -86,6 +94,15 @@ export class WebMask { public update(GL) { const gl = GL.currentContext.GLctx as WebGLRenderingContext; - gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, gl.ALPHA, gl.UNSIGNED_BYTE, this.canvas); + /* #if _WECHAT + const ctx = this.canvas.getContext('2d'); + const canvas = ctx.canvas; + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imgData.width, imgData.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(imgData.data, 0, imgData.data.length)); + wxOffscreenManager.freeCanvas(this.wxFreeNode.id); + //#else */ + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, gl.ALPHA, gl.UNSIGNED_BYTE, this.canvas as any); + // #endif } } diff --git a/web/src/core/wechat-video-reader.ts b/web/src/core/wechat-video-reader.ts new file mode 100644 index 0000000000..95330f22e9 --- /dev/null +++ b/web/src/core/wechat-video-reader.ts @@ -0,0 +1,102 @@ +import { convertMp4 } from '../h264/h264'; + +declare const wx: any; +export class VideoReader { + private readonly frameRate: number; + private currentFrame: number; + private fileTarget: string; + private videoDecoderReady = false; + private videoDecoder: any; + private framebuffer: any; + + public constructor(width, height, frameRate, h264Headers, h264Frames, ptsList) { + this.frameRate = frameRate; + this.currentFrame = -1; + const mp4 = convertMp4(h264Frames, h264Headers, width, height, this.frameRate, ptsList); + const fs = wx.getFileSystemManager(); + const pagPath = `${wx.env.USER_DATA_PATH}/pag/` + this.fileTarget = `${pagPath}${new Date().getTime()}.mp4`; + try { + fs.accessSync(pagPath); + } catch (e) { + try { + fs.mkdirSync(pagPath); + } catch (err) { + console.error(e); + } + } + fs.writeFileSync(this.fileTarget, mp4.buffer, 'utf8'); + this.videoDecoder = wx.createVideoDecoder(); + this.videoDecoderStart(this.fileTarget); + this.videoDecoder.on("ended", async () => { + await this.videoDecoderSeek(0); + this.currentFrame = -1; + }); + this.framebuffer = null; + } + public videoDecoderSeek(position) { + return new Promise((resolve) => { + const onSeeked = (e) => { + this.videoDecoder.off("seek", onSeeked); + resolve(true); + }; + this.videoDecoder.on("seek", onSeeked); + this.videoDecoder.seek(position); + }); + } + public getFrameData() { + return new Promise((resolve) => { + const loop = () => { + const frameData = this.videoDecoder.getFrameData(); + if (frameData !== null) { + resolve(frameData); + return; + } + setTimeout(() => { + loop(); + }, 1); + }; + loop(); + }); + } + public prepareAsync(targetFrame) { + return new Promise(async (resolve) => { + if (targetFrame === this.currentFrame) { + this.currentFrame = targetFrame; + resolve(true); + } else { + if (!this.videoDecoderReady) { + await this.videoDecoderStart(this.fileTarget); + } + /* + if (this.currentFrame !== targetFrame - 1) { + const targetTime = Math.floor(targetFrame * this.frameRate); + await this.videoDecoderSeek(targetTime) + } + */ + this.framebuffer = await this.getFrameData(); + this.currentFrame = targetFrame; + resolve(true); + } + }); + } + public renderToTexture(GL, textureID) { + const gl = GL.currentContext.GLctx; + gl.bindTexture(gl.TEXTURE_2D, GL.textures[textureID]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.framebuffer.width, this.framebuffer.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(this.framebuffer.data, 0, this.framebuffer.data.length)); + } + public onDestroy() { + this.currentFrame = -1; + } + private videoDecoderStart(path) { + return new Promise(resolve => { + const onStarted = () => { + this.videoDecoderReady = true; + this.videoDecoder.off("start", onStarted); + resolve(true); + }; + this.videoDecoder.on("start", onStarted); + this.videoDecoder.start({ source: path, mode: 0 }); + }) + } +} diff --git a/web/src/pag-file.ts b/web/src/pag-file.ts index 08db481c79..85a5c1d377 100644 --- a/web/src/pag-file.ts +++ b/web/src/pag-file.ts @@ -1,6 +1,10 @@ import { PAGComposition } from './pag-composition'; import { PAGImage } from './pag-image'; import { LayerType, PAG, PAGTimeStretchMode, TextDocument } from './types'; +/* #if _WECHAT +import { isWechatMiniProgram } from './utils/ua'; +//#else */ +// #endif import { readFile } from './utils/common'; import { ErrorCode } from './utils/error-map'; import { Log } from './utils/log'; @@ -11,8 +15,12 @@ export class PAGFile extends PAGComposition { /** * Load pag file from file. */ - public static async load(data: File) { + public static async load(data: File & ArrayBuffer) { + /* #if _WECHAT + const buffer = data; + //#else */ const buffer = (await readFile(data)) as ArrayBuffer; + // #endif if (!buffer || !(buffer.byteLength > 0)) Log.errorByCode(ErrorCode.PagFileDataEmpty); const dataUint8Array = new Uint8Array(buffer); const numBytes = dataUint8Array.byteLength * dataUint8Array.BYTES_PER_ELEMENT; diff --git a/web/src/pag-view.ts b/web/src/pag-view.ts index 32c983a221..24dc38a9f7 100644 --- a/web/src/pag-view.ts +++ b/web/src/pag-view.ts @@ -3,29 +3,45 @@ import { PAGPlayer } from './pag-player'; import { EventManager, Listener } from './utils/event-manager'; import { PAGSurface } from './pag-surface'; import { PAGFile } from './pag-file'; +/* #if _WECHAT +import { isWechatMiniProgram } from './utils/ua'; +import { getWechatElementById, requestAnimationFrame, cancelAnimationFrame } from './utils/wechat-babel'; +//#else */ +// #endif +declare const wx; export class PAGView { public static module: PAG; /** * Create pag view. */ - public static async init(file: PAGFile, canvas: string | HTMLCanvasElement | OffscreenCanvas): Promise { - let canvasElement: HTMLCanvasElement; - if (typeof canvas === 'string') { - canvasElement = document.getElementById(canvas.substr(1)) as HTMLCanvasElement; - } else if (canvas instanceof HTMLCanvasElement) { - canvasElement = canvas; - } - canvasElement.style.width = `${canvasElement.width}px`; - canvasElement.style.height = `${canvasElement.height}px`; - canvasElement.width = canvasElement.width * window.devicePixelRatio; - canvasElement.height = canvasElement.height * window.devicePixelRatio; + public static async init(file: PAGFile, canvasID: string): Promise { + /* #if _WECHAT + const canvas = await getWechatElementById(canvasID) + const dpr = wx.getSystemInfoSync().pixelRatio + canvas.width = canvas.width * dpr; + canvas.height = canvas.height * dpr; + const width = canvas.width; + const height = canvas.height; + const gl = canvas.getContext('webgl', { alpha: true }); + const contextID = this.module.GL.registerContext(gl, { majorVersion: 1, minorVersion: 0 }); const pagPlayer = await this.module._PAGPlayer.create(); const pagView = new PAGView(pagPlayer); - const gl = canvasElement.getContext('webgl'); + this.module.GL.makeContextCurrent(contextID); + pagView.pagSurface = await this.module._PAGSurface.FromFrameBuffer(0, width, height, true); + //#else */ + const canvas = document.getElementById(canvasID.substr(1)) as HTMLCanvasElement; + canvas.style.width = `${canvas.width}px`; + canvas.style.height = `${canvas.height}px`; + canvas.width = canvas.width * window.devicePixelRatio; + canvas.height = canvas.height * window.devicePixelRatio; + const pagPlayer = await this.module._PAGPlayer.create(); + const pagView = new PAGView(pagPlayer); + const gl = canvas.getContext('webgl'); const contextID = this.module.GL.registerContext(gl, { majorVersion: 1, minorVersion: 0 }); this.module.GL.makeContextCurrent(contextID); - pagView.pagSurface = await this.module._PAGSurface.FromFrameBuffer(0, canvasElement.width, canvasElement.height, true); + pagView.pagSurface = await this.module._PAGSurface.FromFrameBuffer(0, canvas.width, canvas.height, true); + // #endif pagView.player.setSurface(pagView.pagSurface); pagView.player.setComposition(file); await pagView.setProgress(0); @@ -242,9 +258,15 @@ export class PAGView { if (!this.isPlaying) { return; } + /* #if _WECHAT + this.timer = requestAnimationFrame(async () => { + await this.flushLoop(); + }); + //#else */ this.timer = window.requestAnimationFrame(async () => { await this.flushLoop(); }); + // #endif await this.flushNextFrame(); } @@ -266,7 +288,11 @@ export class PAGView { private clearTimer(): void { if (this.timer) { + /* #if _WECHAT + cancelAnimationFrame(this.timer); + //#else */ window.cancelAnimationFrame(this.timer); + // #endif this.timer = null; } } diff --git a/web/src/types.ts b/web/src/types.ts index b8bed78279..3411572afd 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -4,7 +4,6 @@ import { PAGFile } from './pag-file'; import { PAGImage } from './pag-image'; import { PAGSurface } from './pag-surface'; import { WebAssemblyQueue } from './utils/queue'; -import { VideoReader } from './core/video-reader'; import { ScalerContext } from './core/scaler-context'; import { PAGView } from './pag-view'; import { PAGFont } from './pag-font'; @@ -13,6 +12,11 @@ import { PAGLayer } from './pag-layer'; import { PAGComposition } from './pag-composition'; import { NativeImage } from './core/native-image'; import { WebMask } from './core/web-mask'; +/* #if _WECHAT +import { VideoReader } from './core/wechat-video-reader'; +//#else */ +import { VideoReader } from './core/video-reader'; +// #endif export interface PAG extends EmscriptenModule { _PAGFile: { @@ -26,7 +30,7 @@ export interface PAG extends EmscriptenModule { _PAGSurface: { FromCanvas: (canvasID: string) => Promise; FromTexture: (textureID: number, width: number, height: number, flipY: boolean) => Promise; - FromFrameBuffer: (framebufferID: number, width: number, height: number, flipY: boolean) => Promise; + FromFrameBuffer: (frameBufferID: number, width: number, height: number, flipY: boolean) => Promise; }; VectorString: any; webAssemblyQueue: WebAssemblyQueue; diff --git a/web/src/utils/offscreen-canvas-manager.ts b/web/src/utils/offscreen-canvas-manager.ts new file mode 100644 index 0000000000..37fc78a4c6 --- /dev/null +++ b/web/src/utils/offscreen-canvas-manager.ts @@ -0,0 +1,52 @@ +interface CanvasStackItem { + id: number, + free: boolean, + canvas: any +} + +declare const wx: any; + +export class WXOffscreenManager { + private canvasStacks: CanvasStackItem[]; + public constructor() { + this.canvasStacks = []; + } + public getCanvasNewId() { + return this.canvasStacks.length; + } + public getFreeCanvas() { + const freeNode = this.canvasStacks.find(node => node.free); + if (!freeNode) { + const newNode = this.createFreeCanvas(); + newNode.free = false; + this.canvasStacks.push(newNode); + return newNode; + } else { + freeNode.free = false; + return freeNode; + } + } + public getCanvasNodeById(id = 0) { + return this.canvasStacks.find(node => node.id === id); + } + public createFreeCanvas() { + const canvas = wx.createOffscreenCanvas({ type: '2d' }); + const id = this.getCanvasNewId(); + const stack = { + id, + free: true, + canvas + } + return stack; + } + public freeCanvas(id) { + if(!isNaN(id)) { + const target = this.getCanvasNodeById(id); + if (target) { + target.free = true; + } + } + } +} + +export const wxOffscreenManager = new WXOffscreenManager(); diff --git a/web/src/utils/ua.ts b/web/src/utils/ua.ts index 7ffd5dafae..ce84e1ef40 100644 --- a/web/src/utils/ua.ts +++ b/web/src/utils/ua.ts @@ -1,5 +1,25 @@ + +/* #if _WECHAT +declare const globalThis: any; +export const ANDROID = false; +export const MOBILE = false; +export const MACOS = false +export const IPHONE = false; +export const isWechatMiniProgram = !!globalThis.wx; +// eslint-disable-next-line +globalThis.isWxWebAssembly = false; +if (typeof WebAssembly !== "object" && isWechatMiniProgram) { + // eslint-disable-next-line + globalThis.isWxWebAssembly = true; + // eslint-disable-next-line + globalThis.WebAssembly = WXWebAssembly; +} +//#else */ const nav = navigator.userAgent; export const ANDROID = /android|adr/i.test(nav); export const MOBILE = /(mobile)/i.test(nav) && ANDROID; export const MACOS = !(/(mobile)/i.test(nav) || MOBILE) && /Mac OS X/i.test(nav); export const IPHONE = /(iphone|ipad|ipod)/i.test(nav); +// #endif + + diff --git a/web/src/utils/wechat-babel.ts b/web/src/utils/wechat-babel.ts new file mode 100644 index 0000000000..75a4d41e98 --- /dev/null +++ b/web/src/utils/wechat-babel.ts @@ -0,0 +1,38 @@ +/* #if _WECHAT +import { isWechatMiniProgram } from './ua'; +//#else */ +// #endif + +declare const WXWebAssembly: any; +declare const wx: any; + +export const getWechatElementById = (id: string): Promise => { + return new Promise(resolve => { + const query = wx.createSelectorQuery(); + query.select(`#${id}`).node((res) => { + resolve(res.node) + }).exec() + }) +}; + +export const requestAnimationFrame = (() => { + return function(callback, _lastTime = 0) { + let lastTime = _lastTime; + const currTime = new Date().getTime(); + const timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); + lastTime = currTime + timeToCall; + const id = setTimeout(() => { + callback(currTime + timeToCall, lastTime); + }, timeToCall); + return Number(id); + }; +})(); + +export const cancelAnimationFrame = (() => { + return function(timer) { + return clearTimeout(timer) + }; +})(); + + +