diff --git a/web/package-lock.json b/web/package-lock.json index 35bc2a58ad83..dbb60f0e3819 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -554,6 +554,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -1257,6 +1268,456 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", @@ -6678,6 +7139,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -13697,6 +14168,49 @@ "node": ">=8" } }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13875,6 +14389,19 @@ "npm": ">= 3.0.0" } }, + "node_modules/smol-toml": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz", + "integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -16999,6 +17526,8 @@ "@types/chai": "^5.2.2", "@types/chai-html": "^3.0.0", "json5": "^2.2.3", + "sharp": "^0.34.4", + "smol-toml": "^1.4.2", "ts-node": "^10.9.2", "webpack": "^5.102.0", "webpack-cli": "^6.0.1", diff --git a/web/packages/core/src/internal/player/impl_v1.ts b/web/packages/core/src/internal/player/impl_v1.ts index 3dce7aa9a13c..b112cabbfbc0 100644 --- a/web/packages/core/src/internal/player/impl_v1.ts +++ b/web/packages/core/src/internal/player/impl_v1.ts @@ -41,6 +41,10 @@ export class PlayerV1Impl implements PlayerV1 { this.#inner.play(); } + tick(timestamp: number): void { + this.#inner.tick(timestamp); + } + get isPlaying(): boolean { return this.#inner.isPlaying; } diff --git a/web/packages/core/src/internal/player/inner.tsx b/web/packages/core/src/internal/player/inner.tsx index a5848340ac55..fca04f8ef3eb 100644 --- a/web/packages/core/src/internal/player/inner.tsx +++ b/web/packages/core/src/internal/player/inner.tsx @@ -190,6 +190,10 @@ export class InnerPlayer { // The effective config loaded upon `.load()`. public loadedConfig?: URLLoadOptions | DataLoadOptions; + // Whether content should tick automatically or + // require manual calls to tick(). + public tickAutomatically: boolean = true; + private swfUrl?: URL; private instance: RuffleHandle | null; private newZipWriter: (() => ZipWriter) | null; @@ -652,6 +656,7 @@ export class InnerPlayer { this.newZipWriter = zipWriterClass; configureBuilder(builder, this.loadedConfig || {}); builder.setVolume(this.volumeSettings.get_volume()); + builder.setTickAutomatically(this.tickAutomatically); if (this.loadedConfig?.fontSources) { for (const url of this.loadedConfig.fontSources) { @@ -935,6 +940,11 @@ export class InnerPlayer { this.loadedConfig.backgroundColor; } + this.tickAutomatically = + "__tickAutomatically" in options + ? options["__tickAutomatically"] === true + : true; + await this.ensureFreshInstance(); if ("url" in options) { @@ -972,6 +982,12 @@ export class InnerPlayer { } } + tick(timestamp: number): void { + if (this.instance) { + this.instance.tick_pub(timestamp); + } + } + /** * Whether this player is currently playing. * diff --git a/web/packages/core/src/public/player/v1.ts b/web/packages/core/src/public/player/v1.ts index c94a6362d941..e49ff1486222 100644 --- a/web/packages/core/src/public/player/v1.ts +++ b/web/packages/core/src/public/player/v1.ts @@ -189,4 +189,6 @@ export interface PlayerV1 { * @returns Any value returned by the callback. */ callExternalInterface(name: string, ...args: unknown[]): unknown; + + tick(timestamp: number): void; } diff --git a/web/packages/selfhosted/package.json b/web/packages/selfhosted/package.json index 05dfb88d95d9..27ce10116232 100644 --- a/web/packages/selfhosted/package.json +++ b/web/packages/selfhosted/package.json @@ -25,6 +25,8 @@ "@types/chai": "^5.2.2", "@types/chai-html": "^3.0.0", "json5": "^2.2.3", + "sharp": "^0.34.4", + "smol-toml": "^1.4.2", "ts-node": "^10.9.2", "webpack": "^5.102.0", "webpack-cli": "^6.0.1", diff --git a/web/packages/selfhosted/test/swf_tests/index.html b/web/packages/selfhosted/test/swf_tests/index.html new file mode 100644 index 000000000000..cc50045cc8ed --- /dev/null +++ b/web/packages/selfhosted/test/swf_tests/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
+ + + + + diff --git a/web/packages/selfhosted/test/swf_tests/swf_tests.json5 b/web/packages/selfhosted/test/swf_tests/swf_tests.json5 new file mode 100644 index 000000000000..92612ad107d1 --- /dev/null +++ b/web/packages/selfhosted/test/swf_tests/swf_tests.json5 @@ -0,0 +1,9 @@ +{ + // Tests are loaded from the //tests/tests/swfs directory. + // This file contains web-specific configuration of those tests. + // Please order tests alphabetically. + "tests": { + "visual/edittext/edittext_background_basic": {}, + "visual/edittext/edittext_border_basic": {}, + }, +} diff --git a/web/packages/selfhosted/test/swf_tests/swf_tests.ts b/web/packages/selfhosted/test/swf_tests/swf_tests.ts new file mode 100644 index 000000000000..2afef157e8a9 --- /dev/null +++ b/web/packages/selfhosted/test/swf_tests/swf_tests.ts @@ -0,0 +1,403 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { use } from "chai"; +import chaiHtml from "chai-html"; +import { promises as fs } from "fs"; +import * as path from "path"; +import json5 from "json5"; +import * as toml from "smol-toml"; +import { + getAllTraceOutput, + hideHardwareAccelerationModal, + openTest, + setupAndPlay, + throwIfError, + waitForPlayerToLoad, + waitForRuffle, +} from "../utils"; +import { Player } from "ruffle-core"; +import sharp from "sharp"; +import { MovieMetadata } from "ruffle-core/dist/public/player"; + +const TESTS_DIR = "../../../tests/tests/swfs"; +const TESTS_DESCRIPTOR = "test/swf_tests/swf_tests.json5"; + +use(chaiHtml); + +async function readPng(filePath: string): Promise { + const image = sharp(filePath); + const { data, info } = await image + .raw() + .ensureAlpha() // ensures RGBA + .toBuffer({ resolveWithObject: true }); + + return { + width: info.width, + height: info.height, + data: Array.from(data), + }; +} + +function deconstruct(object: any, prop: string): any | null { + if (!(prop in object)) { + return null; + } + const val = object[prop]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete object[prop]; + return val; +} + +function assertDeconstructedFully(object: any): any { + if (Object.keys(object).length > 0) { + throw Error( + "Expected object to be fully deconstructed, but it was not. " + + "It usually means that some feature used by a SWF test is not " + + "implemented by this runner. Left fields: " + + Object.keys(object), + ); + } +} + +type Trigger = string | number; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface TestDescriptor {} + +interface ImageComparison { + name: string; + filename: string; + checks: ImageComparisonCheck[]; +} + +interface ImageComparisonCheck { + tolerance: number; + outliers: number; +} + +interface Image { + width: number; + height: number; + + // RGBA data + // Note: wdio does not support serializing Uint8ClampedArray + data: Array; +} + +class SwfTest { + name: string; + descriptor: TestDescriptor; + testDir: string; + + loadSucceeded: boolean = false; + + testToml: any; + outputTxt?: string; + inputJson: Array = []; + inputJsonPos: number = 0; + + browser?: WebdriverIO.Browser; + player: ChainablePromiseElement; + + metadata?: MovieMetadata; + frameRate: number = 0; + frameTime: number = 0; + + numTicks: number = 0; + iteration: number = 0; + + sampleCount?: number; + imageComparisons: Map = new Map(); + images: Map = new Map(); + + constructor(name: string, descriptor: TestDescriptor) { + this.name = name; + this.descriptor = descriptor; + this.testDir = path.join(TESTS_DIR, this.name); + } + + async configure() { + const testTomlPath = path.join(this.testDir, "test.toml"); + const inputJsonPath = path.join(this.testDir, "input.json"); + const outputTxtPath = path.join(this.testDir, "output.txt"); + + this.testToml = toml.parse(await fs.readFile(testTomlPath, "utf8")); + this.outputTxt = await fs.readFile(outputTxtPath, "utf8"); + try { + this.inputJson = json5.parse( + await fs.readFile(inputJsonPath, "utf8"), + ); + } catch { + // no input.json + } + + await this.readTestToml(); + } + + async readTestToml() { + const testToml = JSON.parse(JSON.stringify(this.testToml)); + if (!("num_ticks" in testToml)) { + throw Error("Only 'num_ticks' is supported for now."); + } + + this.numTicks = deconstruct(testToml, "num_ticks"); + + if ("image_comparisons" in testToml) { + await this.readTestTomlImageComparisons( + deconstruct(testToml, "image_comparisons"), + ); + } + + if ("player_options" in testToml) { + await this.readTestTomlPlayerOptions( + deconstruct(testToml, "player_options"), + ); + } + + assertDeconstructedFully(testToml); + } + + async readTestTomlImageComparisons(imageComparisons: any) { + for (const name of Object.keys(imageComparisons)) { + const imageComparisonToml = deconstruct(imageComparisons, name); + const trigger = + deconstruct(imageComparisonToml, "trigger") ?? "last_frame"; + + const checks: ImageComparisonCheck[] = []; + checks.push({ + outliers: 0, + tolerance: 0, + }); + + const imageComparison: ImageComparison = { + name, + filename: `${name}.expected.png`, + checks, + }; + + assertDeconstructedFully(imageComparisonToml); + this.imageComparisons.set(trigger, imageComparison); + } + + assertDeconstructedFully(imageComparisons); + } + + async readTestTomlPlayerOptions(playerOptions: any) { + if ("with_renderer" in playerOptions) { + const withRenderer = deconstruct(playerOptions, "with_renderer"); + deconstruct(withRenderer, "optional"); // ignore + + this.sampleCount = deconstruct(withRenderer, "sample_count"); + assertDeconstructedFully(withRenderer); + } + + assertDeconstructedFully(playerOptions); + } + + async load(browser: WebdriverIO.Browser, player: ChainablePromiseElement) { + this.browser = browser; + this.player = player; + + this.metadata = await this.browser!.execute((playerElement) => { + const player = playerElement as Player.PlayerElement; + return player.ruffle().metadata!; + }, this.player); + this.frameRate = this.metadata.frameRate; + this.frameTime = 1000.0 / this.frameRate; + this.loadSucceeded = true; + } + + async runInputForTick() { + while (this.inputJsonPos < this.inputJson.length) { + const event = JSON.parse( + JSON.stringify(this.inputJson[this.inputJsonPos++]), + ); + const eventType = deconstruct(event, "type"); + + switch (eventType) { + case "Wait": + break; + default: + throw new Error(`Unsupported event "${eventType}"`); + } + assertDeconstructedFully(event); + } + } + + async captureImage(trigger: Trigger) { + const canvas = await this.player.shadow$("canvas"); + + const image: Image | undefined = await this.browser!.execute(function ( + canvasElement, + ): Image | undefined { + const canvas = canvasElement as HTMLCanvasElement; + const ctx2d = canvas.getContext("2d"); + if (ctx2d) { + const imageData = ctx2d.getImageData( + 0, + 0, + canvas.width, + canvas.height, + ); + return { + width: canvas.width, + height: canvas.height, + data: Array.from(imageData.data), + }; + } + + const ctxWebgl = + canvas.getContext("webgl") ?? canvas.getContext("webgl2"); + if (ctxWebgl) { + const width = canvas.width; + const height = canvas.height; + const data = new Uint8ClampedArray(width * height * 4); + ctxWebgl.readPixels( + 0, + 0, + width, + height, + ctxWebgl.RGBA, + ctxWebgl.UNSIGNED_BYTE, + data, + ); + + return { + width: canvas.width, + height: canvas.height, + data: Array.from(data), + }; + } + + return undefined; + }, canvas); + + this.images.set(trigger, image); + } + + async tick(timestamp: number) { + await this.browser!.execute( + (playerElement, timestamp) => { + const player = playerElement as Player.PlayerElement; + player.ruffle().tick(timestamp as unknown as number); + }, + this.player, + timestamp, + ); + + if (this.imageComparisons.has(this.iteration)) { + await this.captureImage(this.iteration); + } + + if (this.iteration === this.numTicks) { + await this.captureImage("last_frame"); + } + + this.iteration += 1; + } + + async getActualOutput() { + return (await getAllTraceOutput(this.browser!, this.player)) + .map((line) => line + "\n") + .join(""); + } + + async assertOutput() { + expect(await this.getActualOutput()).toEqual(this.outputTxt); + } + + async assertImage(trigger: Trigger) { + const comparison: ImageComparison = this.imageComparisons.get(trigger)!; + + const expectedImagePath = path.join(this.testDir, comparison.filename); + const expectedImage = await readPng(expectedImagePath); + const actualImage = this.images.get(trigger)!; + + expect([actualImage.width, actualImage.height]).toEqual([ + expectedImage.width, + expectedImage.height, + ]); + // just to make sure + expect(actualImage.data.length).toEqual(expectedImage.data.length); + + // TODO checks + expect(actualImage.data).toEqual(expectedImage.data); + } +} + +async function loadTestsDescriptor(): Promise { + return json5.parse(await fs.readFile(TESTS_DESCRIPTOR, "utf8")); +} + +async function loadTest(name: string, testDescriptor: TestDescriptor) { + const test = new SwfTest(name, testDescriptor); + await test.configure(); + + describe(name, () => { + it("load test", async function () { + const quality = { + 1: "low", + 2: "medium", + 4: "high", + 8: "8x8", + 16: "16x16", + }[test.sampleCount ?? 1]!; + + const swfUrl = `http://localhost:4567/swf_tests/${name}/test.swf`; + await openTest( + browser, + "swf_tests", + `index.html?swf=${encodeURI(swfUrl)}` + + `&config_quality=${encodeURI(quality)}` + + `&name=${encodeURI(name)}`, + ); + await waitForRuffle(browser); + await throwIfError(browser); + + const player = await browser.$(""); + await waitForPlayerToLoad(browser, player); + await setupAndPlay(browser, player); + await hideHardwareAccelerationModal(browser, player); + + await test.load(browser, player); + }); + + it("run test", async function () { + if (!test.loadSucceeded) { + this.skip(); + } + + let timestamp = 0; + await test.tick(timestamp); + + for (let i = 0; i < test.numTicks; i++) { + timestamp += test.frameTime; + await test.runInputForTick(); + await test.tick(timestamp); + } + }); + + it("verify output.txt", async function () { + if (!test.loadSucceeded) { + this.skip(); + } + + await test.assertOutput(); + }); + + for (const [trigger, comparison] of test.imageComparisons) { + it(`verify ${comparison.filename}`, async function () { + if (!test.loadSucceeded) { + this.skip(); + } + + await test.assertImage(trigger); + }); + } + }); +} + +const testsDescriptor = await loadTestsDescriptor(); +for (const key in testsDescriptor["tests"]) { + await loadTest(key, testsDescriptor.tests[key] as TestDescriptor); +} diff --git a/web/packages/selfhosted/test/utils.ts b/web/packages/selfhosted/test/utils.ts index 85577e27b3e5..1d586ac5ff2f 100644 --- a/web/packages/selfhosted/test/utils.ts +++ b/web/packages/selfhosted/test/utils.ts @@ -146,6 +146,16 @@ export async function getTraceOutput( ); } +export async function getAllTraceOutput( + browser: WebdriverIO.Browser, + player: ChainablePromiseElement, +): Promise { + return await browser.execute((playerElement) => { + const player = playerElement as Player.PlayerElement; + return player.__ruffle_log__; + }, player); +} + export async function expectTraceOutput( browser: WebdriverIO.Browser, player: ChainablePromiseElement, diff --git a/web/packages/selfhosted/tsconfig.json b/web/packages/selfhosted/tsconfig.json index 84307e98a7ed..95e0a6d91de8 100644 --- a/web/packages/selfhosted/tsconfig.json +++ b/web/packages/selfhosted/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@tsconfig/strictest/tsconfig.json", "compilerOptions": { "composite": true, - "module": "es2020", + "module": "es2022", "moduleResolution": "node", "target": "es2021", "rootDir": ".", diff --git a/web/packages/selfhosted/wdio.conf.ts b/web/packages/selfhosted/wdio.conf.ts index 30699fc03b4a..edca1ebca45f 100644 --- a/web/packages/selfhosted/wdio.conf.ts +++ b/web/packages/selfhosted/wdio.conf.ts @@ -198,6 +198,7 @@ services.push([ { mount: "/dist", path: "./dist" }, { mount: "/test_assets", path: "./test_assets" }, { mount: "/test", path: "./test" }, + { mount: "/swf_tests", path: "../../../tests/tests/swfs" }, ], port: 4567, }, @@ -219,6 +220,7 @@ export const config: WebdriverIO.Config = { "./test/polyfill/**/test.ts", "./test/js_api/*.ts", "./test/integration_tests/**/test.ts", + "./test/swf_tests/swf_tests.ts", ], maxInstances: maxInstances, capabilities, diff --git a/web/src/builder.rs b/web/src/builder.rs index 998f46233ed5..6392139f087a 100644 --- a/web/src/builder.rs +++ b/web/src/builder.rs @@ -66,6 +66,7 @@ pub struct RuffleInstanceBuilder { pub(crate) gamepad_button_mapping: HashMap, pub(crate) url_rewrite_rules: Vec<(RegExp, String)>, pub(crate) scrolling_behavior: ScrollingBehavior, + pub(crate) tick_automatically: bool, } impl Default for RuffleInstanceBuilder { @@ -105,6 +106,7 @@ impl Default for RuffleInstanceBuilder { gamepad_button_mapping: HashMap::new(), url_rewrite_rules: vec![], scrolling_behavior: ScrollingBehavior::Smart, + tick_automatically: true, } } } @@ -348,6 +350,11 @@ impl RuffleInstanceBuilder { }; } + #[wasm_bindgen(js_name = "setTickAutomatically")] + pub fn set_tick_automatically(&mut self, tick_automatically: bool) { + self.tick_automatically = tick_automatically; + } + // TODO: This should be split into two methods that either load url or load data // Right now, that's done immediately afterwards in TS pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise { diff --git a/web/src/lib.rs b/web/src/lib.rs index 03f4a7fbf9c6..28ac2e5cce3c 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -476,6 +476,10 @@ impl RuffleHandle { pub fn is_wasm_simd_used() -> bool { cfg!(target_feature = "simd128") } + + pub fn tick_pub(&self, timestamp:f64) { + self.tick(timestamp); + } } impl RuffleHandle { @@ -551,11 +555,13 @@ impl RuffleHandle { let shadow_host = Self::get_shadow_host(&parent); Self::set_up_focus_management(ruffle, shadow_host.unwrap_or(parent))?; - // Create the animation frame closure. ruffle.with_instance_mut(|instance| { - instance.animation_handler = Some(Closure::new(move |timestamp| { - ruffle.tick(timestamp); - })); + // Create the animation frame closure. + if config.tick_automatically { + instance.animation_handler = Some(Closure::new(move |timestamp| { + ruffle.tick(timestamp); + })); + } // Create mouse move handler. instance.mouse_move_callback = Some(JsCallback::register( @@ -852,8 +858,10 @@ impl RuffleHandle { )); })?; - // Set initial timestamp and do initial tick to start animation loop. - ruffle.tick(0.0); + if config.tick_automatically { + // Set initial timestamp and do initial tick to start animation loop. + ruffle.tick(0.0); + } Ok(ruffle) }