diff --git a/package-lock.json b/package-lock.json index c7a2276..6f349fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,33 +1,33 @@ { "name": "@diffusionstudio/core", - "version": "1.0.0-rc.7", + "version": "1.0.0-rc.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@diffusionstudio/core", - "version": "1.0.0-rc.7", + "version": "1.0.0-rc.8", "license": "MPL-2.0", "dependencies": { - "mp4-muxer": "^5.1.1" + "mp4-muxer": "^5.1.3" }, "devDependencies": { - "@biomejs/biome": "1.8.3", + "@biomejs/biome": "1.9.2", "@types/dom-webcodecs": "^0.1.11", - "@types/node": "^22.5.2", + "@types/node": "^22.5.5", "@types/wicg-file-system-access": "^2023.10.5", - "@vitest/coverage-v8": "^2.0.5", - "@vitest/web-worker": "^2.0.5", - "@webgpu/types": "^0.1.44", - "jsdom": "^25.0.0", + "@vitest/coverage-v8": "^2.1.1", + "@vitest/web-worker": "^2.1.1", + "@webgpu/types": "^0.1.46", + "jsdom": "^25.0.1", "rollup-plugin-node-externals": "^7.1.3", - "typedoc": "^0.26.6", - "typedoc-plugin-markdown": "^4.2.6", - "typescript": "^5.5.4", + "typedoc": "^0.26.7", + "typedoc-plugin-markdown": "^4.2.8", + "typescript": "^5.6.2", "user-agent-data-types": "^0.4.2", - "vite": "^5.4.2", - "vite-plugin-dts": "^4.1.0", - "vitest": "^2.0.5", + "vite": "^5.4.7", + "vite-plugin-dts": "^4.2.1", + "vitest": "^2.1.1", "vitest-canvas-mock": "^0.3.3" }, "peerDependencies": { @@ -108,9 +108,9 @@ "license": "MIT" }, "node_modules/@biomejs/biome": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.8.3.tgz", - "integrity": "sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.2.tgz", + "integrity": "sha512-4j2Gfwft8Jqp1X0qLYvK4TEy4xhTo4o6rlvJPsjPeEame8gsmbGQfOPBkw7ur+7/Z/f0HZmCZKqbMvR7vTXQYQ==", "dev": true, "hasInstallScript": true, "license": "MIT OR Apache-2.0", @@ -125,20 +125,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.8.3", - "@biomejs/cli-darwin-x64": "1.8.3", - "@biomejs/cli-linux-arm64": "1.8.3", - "@biomejs/cli-linux-arm64-musl": "1.8.3", - "@biomejs/cli-linux-x64": "1.8.3", - "@biomejs/cli-linux-x64-musl": "1.8.3", - "@biomejs/cli-win32-arm64": "1.8.3", - "@biomejs/cli-win32-x64": "1.8.3" + "@biomejs/cli-darwin-arm64": "1.9.2", + "@biomejs/cli-darwin-x64": "1.9.2", + "@biomejs/cli-linux-arm64": "1.9.2", + "@biomejs/cli-linux-arm64-musl": "1.9.2", + "@biomejs/cli-linux-x64": "1.9.2", + "@biomejs/cli-linux-x64-musl": "1.9.2", + "@biomejs/cli-win32-arm64": "1.9.2", + "@biomejs/cli-win32-x64": "1.9.2" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz", - "integrity": "sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.2.tgz", + "integrity": "sha512-rbs9uJHFmhqB3Td0Ro+1wmeZOHhAPTL3WHr8NtaVczUmDhXkRDWScaxicG9+vhSLj1iLrW47itiK6xiIJy6vaA==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz", - "integrity": "sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.2.tgz", + "integrity": "sha512-BlfULKijNaMigQ9GH9fqJVt+3JTDOSiZeWOQtG/1S1sa8Lp046JHG3wRJVOvekTPL9q/CNFW1NVG8J0JN+L1OA==", "cpu": [ "x64" ], @@ -170,9 +170,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz", - "integrity": "sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.2.tgz", + "integrity": "sha512-T8TJuSxuBDeQCQzxZu2o3OU4eyLumTofhCxxFd3+aH2AEWVMnH7Z/c3QP1lHI5RRMBP9xIJeMORqDQ5j+gVZzw==", "cpu": [ "arm64" ], @@ -187,9 +187,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz", - "integrity": "sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.2.tgz", + "integrity": "sha512-ZATvbUWhNxegSALUnCKWqetTZqrK72r2RsFD19OK5jXDj/7o1hzI1KzDNG78LloZxftrwr3uI9SqCLh06shSZw==", "cpu": [ "arm64" ], @@ -204,9 +204,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz", - "integrity": "sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.2.tgz", + "integrity": "sha512-T0cPk3C3Jr2pVlsuQVTBqk2qPjTm8cYcTD9p/wmR9MeVqui1C/xTVfOIwd3miRODFMrJaVQ8MYSXnVIhV9jTjg==", "cpu": [ "x64" ], @@ -221,9 +221,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz", - "integrity": "sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.2.tgz", + "integrity": "sha512-CjPM6jT1miV5pry9C7qv8YJk0FIZvZd86QRD3atvDgfgeh9WQU0k2Aoo0xUcPdTnoz0WNwRtDicHxwik63MmSg==", "cpu": [ "x64" ], @@ -238,9 +238,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz", - "integrity": "sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.2.tgz", + "integrity": "sha512-2x7gSty75bNIeD23ZRPXyox6Z/V0M71ObeJtvQBhi1fgrvPdtkEuw7/0wEHg6buNCubzOFuN9WYJm6FKoUHfhg==", "cpu": [ "arm64" ], @@ -255,9 +255,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz", - "integrity": "sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.2.tgz", + "integrity": "sha512-JC3XvdYcjmu1FmAehVwVV0SebLpeNTnO2ZaMdGCSOdS7f8O9Fq14T2P1gTG1Q29Q8Dt1S03hh0IdVpIZykOL8g==", "cpu": [ "x64" ], @@ -744,19 +744,19 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.47.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.4.tgz", - "integrity": "sha512-HKm+P4VNzWwvq1Ey+Jfhhj/3MjsD+ka2hbt8L5AcRM95lu1MFOYnz3XlU7Gr79Q/ZhOb7W/imAKeYrOI0bFydg==", + "version": "7.47.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.7.tgz", + "integrity": "sha512-fNiD3G55ZJGhPOBPMKD/enozj8yxJSYyVJWxRWdcUtw842rvthDHJgUWq9gXQTensFlMHv2wGuCjjivPv53j0A==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.29.4", + "@microsoft/api-extractor-model": "7.29.6", "@microsoft/tsdoc": "~0.15.0", "@microsoft/tsdoc-config": "~0.17.0", - "@rushstack/node-core-library": "5.5.1", + "@rushstack/node-core-library": "5.7.0", "@rushstack/rig-package": "0.5.3", - "@rushstack/terminal": "0.13.3", - "@rushstack/ts-command-line": "4.22.3", + "@rushstack/terminal": "0.14.0", + "@rushstack/ts-command-line": "4.22.6", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", @@ -769,15 +769,15 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.29.4.tgz", - "integrity": "sha512-LHOMxmT8/tU1IiiiHOdHFF83Qsi+V8d0kLfscG4EvQE9cafiR8blOYr8SfkQKWB1wgEilQgXJX3MIA4vetDLZw==", + "version": "7.29.6", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.29.6.tgz", + "integrity": "sha512-gC0KGtrZvxzf/Rt9oMYD2dHvtN/1KPEYsrQPyMKhLHnlVuO/f4AFN3E4toqZzD2pt4LhkKoYmL2H9tX3yCOyRw==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.15.0", "@microsoft/tsdoc-config": "~0.17.0", - "@rushstack/node-core-library": "5.5.1" + "@rushstack/node-core-library": "5.7.0" } }, "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { @@ -1133,9 +1133,9 @@ ] }, "node_modules/@rushstack/node-core-library": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.5.1.tgz", - "integrity": "sha512-ZutW56qIzH8xIOlfyaLQJFx+8IBqdbVCZdnj+XT1MorQ1JqqxHse8vbCpEM+2MjsrqcbxcgDIbfggB1ZSQ2A3g==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.7.0.tgz", + "integrity": "sha512-Ff9Cz/YlWu9ce4dmqNBZpA45AEya04XaBFIjV7xTVeEf+y/kTjEasmozqFELXlNG4ROdevss75JrrZ5WgufDkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1215,13 +1215,13 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.13.3.tgz", - "integrity": "sha512-fc3zjXOw8E0pXS5t9vTiIPx9gHA0fIdTXsu9mT4WbH+P3mYvnrX0iAQ5a6NvyK1+CqYWBTw/wVNx7SDJkI+WYQ==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.0.tgz", + "integrity": "sha512-juTKMAMpTIJKudeFkG5slD8Z/LHwNwGZLtU441l/u82XdTBfsP+LbGKJLCNwP5se+DMCT55GB8x9p6+C4UL7jw==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.5.1", + "@rushstack/node-core-library": "5.7.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1250,13 +1250,13 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.3.tgz", - "integrity": "sha512-edMpWB3QhFFZ4KtSzS8WNjBgR4PXPPOVrOHMbb7kNpmQ1UFS9HdVtjCXg1H5fG+xYAbeE+TMPcVPUyX2p84STA==", + "version": "4.22.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.6.tgz", + "integrity": "sha512-QSRqHT/IfoC5nk9zn6+fgyqOPXHME0BfchII9EUPR19pocsNp/xSbeBCbD3PIR2Lg+Q5qk7OFqk1VhWPMdKHJg==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.13.3", + "@rushstack/terminal": "0.14.0", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -1342,9 +1342,9 @@ } }, "node_modules/@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", "dev": true, "license": "MIT", "dependencies": { @@ -1366,20 +1366,20 @@ "license": "MIT" }, "node_modules/@vitest/coverage-v8": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", - "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.5", + "debug": "^4.3.6", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.10", + "magic-string": "^0.30.11", "magicast": "^0.3.4", "std-env": "^3.7.0", "test-exclude": "^7.0.1", @@ -1389,18 +1389,24 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.0.5" + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, "node_modules/@vitest/expect": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", - "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -1408,10 +1414,48 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/@vitest/pretty-format": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", - "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1422,13 +1466,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", - "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.0.5", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -1436,14 +1480,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", - "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.0.5", - "magic-string": "^0.30.10", + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", "pathe": "^1.1.2" }, "funding": { @@ -1451,9 +1495,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", - "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1464,14 +1508,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", - "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.0.5", - "estree-walker": "^3.0.3", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -1479,84 +1522,74 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vitest/web-worker": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-2.0.5.tgz", - "integrity": "sha512-V569KT8CAeh/Cj8JE1oG8FZ7ajmcqSZQ5bhCMwYsYxeceDcV9iDDkRgQFWOJX7yOI95ldiUOaQdFpLrc2eBREA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-2.1.1.tgz", + "integrity": "sha512-9APtqy5bmpD9l7q6NZ3iN3w3cUd7IBHH5+8VvrwedqItomz7JLeb5Mj0E1Pfh9Jo3thI87ClipY+aL2HROmzfA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5" + "debug": "^4.3.6" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.0.5" + "vitest": "2.1.1" } }, "node_modules/@volar/language-core": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.2.tgz", - "integrity": "sha512-sONt5RLvLL1SlBdhyUSthZzuKePbJ7DwFFB9zT0eyWpDl+v7GXGh/RkPxxWaR22bIhYtTzp4Ka1MWatl/53Riw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz", + "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.2" + "@volar/source-map": "2.4.5" } }, "node_modules/@volar/source-map": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.2.tgz", - "integrity": "sha512-qiGfGgeZ5DEarPX3S+HcFktFCjfDrFPCXKeXNbrlB7v8cvtPRm8YVwoXOdGG1NhaL5rMlv5BZPVQyu4EdWWIvA==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz", + "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.2.tgz", - "integrity": "sha512-m2uZduhaHO1SZuagi30OsjI/X1gwkaEAC+9wT/nCNAtJ5FqXEkKvUncHmffG7ESDZPlFFUBK4vJ0D9Hfr+f2EA==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz", + "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.2", + "@volar/language-core": "2.4.5", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.3.tgz", - "integrity": "sha512-adAfy9boPkP233NTyvLbGEqVuIfK/R0ZsBsIOW4BZNfb4BRpRW41Do1u+ozJpsb+mdoy80O20IzAsHaihRb5qA==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.8.tgz", + "integrity": "sha512-Uzlxp91EPjfbpeO5KtC0KnXPkuTfGsNDeaKQJxQN718uz+RqDYarEf7UhQJGK+ZYloD2taUbHTI2J4WrUaZQNA==", "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.3", + "@vue/shared": "3.5.8", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.3.tgz", - "integrity": "sha512-wnzFArg9zpvk/811CDOZOadJRugf1Bgl/TQ3RfV4nKfSPok4hi0w10ziYUQR6LnnBAUlEXYLUfZ71Oj9ds/+QA==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.8.tgz", + "integrity": "sha512-GUNHWvoDSbSa5ZSHT9SnV5WkStWfzJwwTd6NMGzilOE/HM5j+9EB9zGXdtu/fCNEmctBqMs6C9SvVPpVPuk1Eg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.3", - "@vue/shared": "3.5.3" + "@vue/compiler-core": "3.5.8", + "@vue/shared": "3.5.8" } }, "node_modules/@vue/compiler-vue2": { @@ -1571,13 +1604,13 @@ } }, "node_modules/@vue/language-core": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz", - "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "~2.4.0-alpha.18", + "@volar/language-core": "~2.4.1", "@vue/compiler-dom": "^3.4.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.4.0", @@ -1596,16 +1629,16 @@ } }, "node_modules/@vue/shared": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.3.tgz", - "integrity": "sha512-Jp2v8nylKBT+PlOUjun2Wp/f++TfJVFjshLzNtJDdmFJabJa7noGMncqXRM1vXGX+Yo2V7WykQFNxusSim8SCA==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.8.tgz", + "integrity": "sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==", "dev": true, "license": "MIT" }, "node_modules/@webgpu/types": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", - "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", + "version": "0.1.46", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.46.tgz", + "integrity": "sha512-2iogO6Zh0pTbKLGZuuGWEmJpF/fTABGs7G9wXxpn7s24XSJchSUIiMqIJHURi5zsMZRRTuXrV/3GLOkmOFjq5w==", "license": "BSD-3-Clause" }, "node_modules/@xmldom/xmldom": { @@ -2047,30 +2080,6 @@ "license": "MIT", "peer": true }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2125,16 +2134,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2170,19 +2169,6 @@ "node": "*" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2292,16 +2278,6 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2358,19 +2334,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2474,13 +2437,13 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz", - "integrity": "sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", "dependencies": { - "cssstyle": "^4.0.1", + "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", @@ -2493,7 +2456,7 @@ "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.4", + "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", @@ -2659,13 +2622,6 @@ "dev": true, "license": "MIT" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2689,19 +2645,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2752,9 +2695,9 @@ } }, "node_modules/mp4-muxer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/mp4-muxer/-/mp4-muxer-5.1.1.tgz", - "integrity": "sha512-6RfRE+TFfOK7+HyIr7j9v2LVNYgzDPdqNYLAaJifGWZmE072gvW+DjXxuxBhinSloI/rJq5AcPmUOGtCvBPw2Q==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/mp4-muxer/-/mp4-muxer-5.1.3.tgz", + "integrity": "sha512-artYS8Y7l9Cgqi1WyoNZtHW+pXOM0Tg75eupENoSEx59hMJlBqXxsTgJQ8T0ycrb6h9lw9iNKVB8cKEn7HjG2A==", "license": "MIT", "dependencies": { "@types/dom-webcodecs": "^0.1.6", @@ -2800,35 +2743,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nwsapi": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", @@ -2836,22 +2750,6 @@ "dev": true, "license": "MIT" }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -3028,13 +2926,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3055,13 +2946,6 @@ "node": ">=6" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3072,13 +2956,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3406,19 +3283,6 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3487,6 +3351,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinypool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", @@ -3508,15 +3379,35 @@ } }, "node_modules/tinyspy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", - "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.47", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz", + "integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.47" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.47", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz", + "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -3528,19 +3419,16 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" + "node": ">=16" } }, "node_modules/tr46": { @@ -3557,17 +3445,17 @@ } }, "node_modules/typedoc": { - "version": "0.26.6", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", - "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.7.tgz", + "integrity": "sha512-gUeI/Wk99vjXXMi8kanwzyhmeFEGv1LTdTQsiyIsmSYsBebvFxhbcyAx7Zjo4cMbpLGxM4Uz3jVIjksu/I2v6Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "shiki": "^1.9.1", - "yaml": "^2.4.5" + "shiki": "^1.16.2", + "yaml": "^2.5.1" }, "bin": { "typedoc": "bin/typedoc" @@ -3576,13 +3464,13 @@ "node": ">= 18" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" } }, "node_modules/typedoc-plugin-markdown": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.7.tgz", - "integrity": "sha512-bLsQdweSm48P9j6kGqQ3/4GCH5zu2EnURSkkxqirNc+uVFE9YK825ogDw+WbNkRHIV6eZK/1U43gT7YfglyYOg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.8.tgz", + "integrity": "sha512-1EDsc66jaCjZtxdYy+Rl0KDU1WY/iyuCOOPaeFzcYFZ81FNXV8CmgUDOHri20WGmYnkEM5nQ+ooxj1vyuQo0Lg==", "dev": true, "license": "MIT", "engines": { @@ -3593,9 +3481,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3628,9 +3516,9 @@ "license": "MIT" }, "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -3647,17 +3535,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/user-agent-data-types": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/user-agent-data-types/-/user-agent-data-types-0.4.2.tgz", @@ -3666,9 +3543,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz", - "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3726,16 +3603,15 @@ } }, "node_modules/vite-node": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", - "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.5", + "debug": "^4.3.6", "pathe": "^1.1.2", - "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -3749,22 +3625,21 @@ } }, "node_modules/vite-plugin-dts": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.1.1.tgz", - "integrity": "sha512-SxYXwJQbAZ1IMtGEcOuzzZtDWCdcV2JkU7esvpPA8E5tIWVcJB42rZwN9EdULicWGLfaXrUgPIGVSidXBTae2Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.2.1.tgz", + "integrity": "sha512-/QlYvgUMiv8+ZTEerhNCYnYaZMM07cdlX6hQCR/w/g/nTh0tUXPoYwbT6SitizLJ9BybT1lnrcZgqheI6wromQ==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor": "7.47.4", + "@microsoft/api-extractor": "7.47.7", "@rollup/pluginutils": "^5.1.0", - "@volar/typescript": "^2.3.4", - "@vue/language-core": "2.0.29", + "@volar/typescript": "^2.4.4", + "@vue/language-core": "2.1.6", "compare-versions": "^6.1.1", "debug": "^4.3.6", "kolorist": "^1.8.0", "local-pkg": "^0.5.0", - "magic-string": "^0.30.11", - "vue-tsc": "2.0.29" + "magic-string": "^0.30.11" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3780,30 +3655,30 @@ } }, "node_modules/vitest": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", - "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@vitest/expect": "2.0.5", - "@vitest/pretty-format": "^2.0.5", - "@vitest/runner": "2.0.5", - "@vitest/snapshot": "2.0.5", - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", - "debug": "^4.3.5", - "execa": "^8.0.1", - "magic-string": "^0.30.10", + "debug": "^4.3.6", + "magic-string": "^0.30.11", "pathe": "^1.1.2", "std-env": "^3.7.0", - "tinybench": "^2.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.0.5", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -3818,8 +3693,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.0.5", - "@vitest/ui": "2.0.5", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", "happy-dom": "*", "jsdom": "*" }, @@ -3864,24 +3739,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vue-tsc": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz", - "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "~2.4.0-alpha.18", - "@vue/language-core": "2.0.29", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index da1fc01..f9e5f38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@diffusionstudio/core", "private": false, - "version": "1.0.0-rc.7", + "version": "1.0.0-rc.8", "type": "module", "description": "Build bleeding edge video processing applications", "files": [ @@ -27,26 +27,26 @@ "docs": "typedoc src/index.ts --plugin typedoc-plugin-markdown --out ./docs" }, "devDependencies": { - "@biomejs/biome": "1.8.3", + "@biomejs/biome": "1.9.2", "@types/dom-webcodecs": "^0.1.11", - "@types/node": "^22.5.2", + "@types/node": "^22.5.5", "@types/wicg-file-system-access": "^2023.10.5", - "@vitest/coverage-v8": "^2.0.5", - "@vitest/web-worker": "^2.0.5", - "@webgpu/types": "^0.1.44", - "jsdom": "^25.0.0", + "@vitest/coverage-v8": "^2.1.1", + "@vitest/web-worker": "^2.1.1", + "@webgpu/types": "^0.1.46", + "jsdom": "^25.0.1", "rollup-plugin-node-externals": "^7.1.3", - "typedoc": "^0.26.6", - "typedoc-plugin-markdown": "^4.2.6", - "typescript": "^5.5.4", + "typedoc": "^0.26.7", + "typedoc-plugin-markdown": "^4.2.8", + "typescript": "^5.6.2", "user-agent-data-types": "^0.4.2", - "vite": "^5.4.2", - "vite-plugin-dts": "^4.1.0", - "vitest": "^2.0.5", + "vite": "^5.4.7", + "vite-plugin-dts": "^4.2.1", + "vitest": "^2.1.1", "vitest-canvas-mock": "^0.3.3" }, "dependencies": { - "mp4-muxer": "^5.1.1" + "mp4-muxer": "^5.1.3" }, "peerDependencies": { "pixi-filters": ">=6.0.0", diff --git a/src/clips/clip/clip.deserializer.spec.ts b/src/clips/clip/clip.deserializer.spec.ts new file mode 100644 index 0000000..7594164 --- /dev/null +++ b/src/clips/clip/clip.deserializer.spec.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { ClipDeserializer } from './clip.desierializer'; +import { + AudioClip, + VideoClip, + HtmlClip, + ImageClip, + TextClip, + ComplexTextClip, + Clip +} from '..'; +import { AudioSource, HtmlSource, ImageSource, VideoSource } from '../../sources'; +import type { Source } from '../../sources'; + +describe('ClipDeserializer', () => { + it('should return correct clip based on type', () => { + expect(ClipDeserializer.fromType({ type: 'video' })).toBeInstanceOf(VideoClip); + expect(ClipDeserializer.fromType({ type: 'audio' })).toBeInstanceOf(AudioClip); + expect(ClipDeserializer.fromType({ type: 'html' })).toBeInstanceOf(HtmlClip); + expect(ClipDeserializer.fromType({ type: 'image' })).toBeInstanceOf(ImageClip); + expect(ClipDeserializer.fromType({ type: 'text' })).toBeInstanceOf(TextClip); + expect(ClipDeserializer.fromType({ type: 'complex_text' })).toBeInstanceOf(ComplexTextClip); + expect(ClipDeserializer.fromType({ type: 'unknown' as any })).toBeInstanceOf(Clip); // Default case + }); + + it('should return correct clip based on source', () => { + // Mock instances for different source types + const audioSource = new AudioSource(); + const videoSource = new VideoSource(); + const imageSource = new ImageSource(); + const htmlSource = new HtmlSource(); + + const res = ClipDeserializer.fromSource(audioSource) + + // Ensure proper class instantiation based on source type + expect(res).toBeInstanceOf(AudioClip); + expect(ClipDeserializer.fromSource(videoSource)).toBeInstanceOf(VideoClip); + expect(ClipDeserializer.fromSource(imageSource)).toBeInstanceOf(ImageClip); + expect(ClipDeserializer.fromSource(htmlSource)).toBeInstanceOf(HtmlClip); + }); + + it('should return undefined if source type does not match', () => { + const invalidSourceMock = { type: 'unknown' } as any as Source; + expect(ClipDeserializer.fromSource(invalidSourceMock)).toBeUndefined(); + }); +}); diff --git a/src/clips/video/buffer.spec.ts b/src/clips/video/buffer.spec.ts new file mode 100644 index 0000000..e64d993 --- /dev/null +++ b/src/clips/video/buffer.spec.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { FrameBuffer } from './buffer'; + +describe('FrameBuffer', () => { + let frameBuffer: FrameBuffer; + let mockVideoFrame: any; + + beforeEach(() => { + // Mock VideoFrame + mockVideoFrame = { + close: vi.fn(), + }; + + frameBuffer = new FrameBuffer(); + }); + + it('should enqueue frames and trigger onenqueue callback', () => { + const mockOnEnqueue = vi.fn(); + frameBuffer.onenqueue = mockOnEnqueue; + + frameBuffer.enqueue(mockVideoFrame); + + expect(frameBuffer['buffer'].length).toBe(1); + expect(frameBuffer['buffer'][0]).toBe(mockVideoFrame); + expect(mockOnEnqueue).toHaveBeenCalled(); + }); + + it('should dequeue frames in FIFO order', async () => { + const frame1 = { ...mockVideoFrame }; + const frame2 = { ...mockVideoFrame }; + + frameBuffer.enqueue(frame1); + frameBuffer.enqueue(frame2); + + const dequeuedFrame1 = await frameBuffer.dequeue(); + const dequeuedFrame2 = await frameBuffer.dequeue(); + + expect(dequeuedFrame1).toBe(frame1); + expect(dequeuedFrame2).toBe(frame2); + expect(frameBuffer['buffer'].length).toBe(0); + }); + + it('should wait for a frame to be enqueued if buffer is empty and state is active', async () => { + const mockOnEnqueue = vi.fn(); + const mockWaitFor = vi.spyOn(frameBuffer as any, 'waitFor'); + + frameBuffer.onenqueue = mockOnEnqueue; + const dequeuePromise = frameBuffer.dequeue(); + + // Simulate enqueuing a frame after some delay + setTimeout(() => { + frameBuffer.enqueue(mockVideoFrame); + }, 100); + + const result = await dequeuePromise; + + expect(result).toBe(mockVideoFrame); + expect(mockWaitFor).toHaveBeenCalledWith(20000); // 20s timeout + }); + + it('should resolve immediately if buffer is closed and empty', async () => { + frameBuffer.close(); + + const result = await frameBuffer.dequeue(); + expect(result).toBeUndefined(); + }); + + it('should call onclose callback when buffer is closed', () => { + const mockOnClose = vi.fn(); + frameBuffer.onclose = mockOnClose; + + frameBuffer.close(); + + expect(frameBuffer['state']).toBe('closed'); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('should close all frames when terminate is called', () => { + const frame1 = { ...mockVideoFrame, close: vi.fn() }; + const frame2 = { ...mockVideoFrame, close: vi.fn() }; + + frameBuffer.enqueue(frame1); + frameBuffer.enqueue(frame2); + + frameBuffer.terminate(); + + expect(frame1.close).toHaveBeenCalled(); + expect(frame2.close).toHaveBeenCalled(); + }); + + it('should reject after timeout if no enqueue or close happens', async () => { + await expect((frameBuffer as any).waitFor(50)).rejects.toThrow('Promise timed out after 50 ms'); + }); +}); diff --git a/src/clips/video/decoder.spec.ts b/src/clips/video/decoder.spec.ts new file mode 100644 index 0000000..c95166d --- /dev/null +++ b/src/clips/video/decoder.spec.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { Decoder } from './decoder'; + +describe('Decoder', () => { + let mockPostMessage: Mock + let mockVideoDecoder: Mock + let mockVideoFrame: any; + let decoder: Decoder; + + beforeEach(() => { + // Mock postMessage for 'self' + mockPostMessage = vi.fn(); + (global as any).self = { + postMessage: mockPostMessage, + close: vi.fn(), + }; + + // Mock VideoDecoder + mockVideoDecoder = vi.fn().mockImplementation(({ output, error }) => { + return { + output, + error, + decode: vi.fn(), + close: vi.fn(), + }; + }); + (global as any).VideoDecoder = mockVideoDecoder; + + // Mock VideoFrame + mockVideoFrame = { + timestamp: 0, + duration: 1000000, // 1 second in nanoseconds + close: vi.fn(), + }; + }); + + it('should initialize with correct properties', () => { + const range = [0, 5] satisfies [number, number]; // 5 seconds range + const fps = 30; + + decoder = new Decoder(range, fps); + + expect(decoder.video).toBeDefined(); + expect(mockVideoDecoder).toHaveBeenCalled(); + expect(decoder['currentTime']).toBe(range[0] * 1e6); + expect(decoder['firstTimestamp']).toBe(range[0] * 1e6); + expect(decoder['totalFrames']).toBe(((range[1] - range[0]) * fps) + 1); + expect(decoder['fps']).toBe(fps); + }); + + it('should post a frame and update current time and count', () => { + const range = [0, 5] satisfies [number, number]; + const fps = 30; + + decoder = new Decoder(range, fps); + + decoder['postFrame'](mockVideoFrame); + + expect(mockPostMessage).toHaveBeenCalledWith({ type: 'frame', frame: mockVideoFrame }); + expect(decoder['currentTime']).toBeGreaterThan(range[0] * 1e6); // Time should increase + expect(decoder['currentFrames']).toBe(1); + }); + + it('should handle frame output within range and post frames', () => { + const range = [0, 5] satisfies [number, number]; + const fps = 30; + + decoder = new Decoder(range, fps); + mockVideoFrame.timestamp = range[0] * 1e6; // Start time + + decoder['handleFrameOutput'](mockVideoFrame); + + expect(mockPostMessage).toHaveBeenCalledWith({ type: 'frame', frame: mockVideoFrame }); + expect(mockVideoFrame.close).toHaveBeenCalled(); + }); + + it('should handle errors and post error messages', () => { + const range = [0, 5] satisfies [number, number]; + const fps = 30; + const mockError = new DOMException('Test Error'); + + decoder = new Decoder(range, fps); + + decoder['handleError'](mockError); + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'error', + message: 'Test Error', + }); + expect(self.close).toHaveBeenCalled(); + }); +}); diff --git a/src/clips/video/demuxer/ffmpeg.worker.ts b/src/clips/video/demuxer/ffmpeg.worker.ts index 68701d4..3916ab2 100644 --- a/src/clips/video/demuxer/ffmpeg.worker.ts +++ b/src/clips/video/demuxer/ffmpeg.worker.ts @@ -1,3 +1,4 @@ +/* c8 ignore start */ import type { WebAVPacket, WebAVStream } from './types'; let Module: any; // TODO: rm any diff --git a/src/clips/video/demuxer/types/avutil.ts b/src/clips/video/demuxer/types/avutil.ts index abd16c8..170dfce 100644 --- a/src/clips/video/demuxer/types/avutil.ts +++ b/src/clips/video/demuxer/types/avutil.ts @@ -1,3 +1,4 @@ +/* c8 ignore start */ /** * sync with ffmpeg libavutil/avutil.h */ diff --git a/src/clips/video/demuxer/types/demuxer.ts b/src/clips/video/demuxer/types/demuxer.ts index f16aa26..eaabbfc 100644 --- a/src/clips/video/demuxer/types/demuxer.ts +++ b/src/clips/video/demuxer/types/demuxer.ts @@ -1,3 +1,4 @@ +/* c8 ignore start */ /** * sync with web-demuxer.h */ diff --git a/src/clips/video/demuxer/types/ffmpeg-worker-message.ts b/src/clips/video/demuxer/types/ffmpeg-worker-message.ts index 7425366..171ddf0 100644 --- a/src/clips/video/demuxer/types/ffmpeg-worker-message.ts +++ b/src/clips/video/demuxer/types/ffmpeg-worker-message.ts @@ -1,3 +1,4 @@ +/* c8 ignore start */ import type { AVMediaType } from './avutil'; export enum FFMpegWorkerMessageType { diff --git a/src/clips/video/demuxer/web-demuxer.ts b/src/clips/video/demuxer/web-demuxer.ts index 3f6541e..8e78148 100644 --- a/src/clips/video/demuxer/web-demuxer.ts +++ b/src/clips/video/demuxer/web-demuxer.ts @@ -1,3 +1,4 @@ +/* c8 ignore start */ import { AVMediaType, FFMpegWorkerMessageType } from './types'; import FFmpegWorker from './ffmpeg.worker.ts?worker&inline'; diff --git a/src/clips/video/video.spec.ts b/src/clips/video/video.spec.ts index 008b62b..a504397 100644 --- a/src/clips/video/video.spec.ts +++ b/src/clips/video/video.spec.ts @@ -11,6 +11,8 @@ import { Source, VideoSource } from '../../sources'; import { VideoClip } from './video'; import { Composition } from '../../composition'; import { Keyframe, Timestamp } from '../../models'; +import { sleep } from '../../utils'; +import { FrameBuffer } from './buffer'; import type { MockInstance } from 'vitest'; @@ -24,7 +26,7 @@ describe('The Video Clip', () => { let playFn: MockInstance<() => Promise>; let pauseFn: MockInstance<() => Promise>; - let seekFn: MockInstance<(time: Timestamp) => Promise>; + let seekFn: MockInstance<(arg: number) => void>; const createObjectUrlSpy = vi.spyOn(Source.prototype as any, 'createObjectURL'); @@ -95,8 +97,8 @@ describe('The Video Clip', () => { it('should connect to a track', async () => { clip - .set({ offset: 10, height: '100%' }) - .subclip(10, 80); + .set({ offset: 6, height: '100%' }) + .subclip(6, 80); const composition = new Composition(); const track = composition.createTrack('video'); @@ -106,17 +108,18 @@ describe('The Video Clip', () => { await track.add(clip); expect(seekFn).toHaveBeenCalledTimes(1); - expect(seekFn.mock.calls[0][0].seconds).toBe(1); + // composition.frame - offset = 24; 24 / 30fps = 0.8 + expect(seekFn.mock.calls[0][0]).toBe(0.8); expect(clip.state).toBe('ATTACHED'); - expect(clip.offset.frames).toBe(10); + expect(clip.offset.frames).toBe(6); expect(clip.duration.seconds).toBe(30); expect(clip.height).toBe('100%'); - expect(clip.range[0].frames).toBe(10); + expect(clip.range[0].frames).toBe(6); expect(clip.range[1].frames).toBe(80); - expect(clip.start.frames).toBe(20); - expect(clip.stop.frames).toBe(90); + expect(clip.start.frames).toBe(12); + expect(clip.stop.frames).toBe(86); expect(track.clips.findIndex((n) => n.id == clip.id)).toBe(0); expect(attachFn).toBeCalledTimes(1); @@ -151,6 +154,7 @@ describe('The Video Clip', () => { expect(updateSpy).toHaveBeenCalledTimes(1); expect(exitSpy).toHaveBeenCalledTimes(0); + pauseFn.mockClear(); composition.state = 'IDLE'; composition.frame = 60; composition.computeFrame(); @@ -255,6 +259,60 @@ describe('The Video Clip', () => { expect(clip.sprite.texture.uid).toBe(clip.textrues.html5.uid); }); + + it('should start decoding the video when the seek method is called and the composition is rendering', async () => { + const composition = new Composition(); + await composition.add(clip); + + const buffer = new FrameBuffer(); + + Object.defineProperty(buffer, 'onenqueue', { + set: (fn: () => void) => fn() + }); + + //@ts-ignore + const decodeSpy = vi.spyOn(clip, 'decodeVideo').mockReturnValueOnce(buffer); + composition.state = 'RENDER'; + + await clip.seek(new Timestamp()); + + expect(decodeSpy).toBeCalledTimes(1); + expect(seekFn.mock.calls[0][0]).toBe(0); + }); + + it('should calculate the correct demux range', async () => { + const composition = new Composition(); + await composition.add(clip); + + clip.subclip(6, 63); + + let [start, stop] = (clip as any).demuxRange; + expect(start).toBe(0.2); + expect(stop).toBe(2.1); + + clip.offsetBy(-12) + + composition.duration = 30; + + [start, stop] = (clip as any).demuxRange; + expect(start).toBe(0.4); + expect(stop).toBe(1.4); + }); + + it('should be able to cancel decoding', async () => { + const workerSpy = vi.fn(); + const bufferSpy = vi.fn(); + + // @ts-ignore + clip.worker = { terminate: workerSpy }; + // @ts-ignore + clip.buffer = { terminate: bufferSpy }; + + clip.cancelDecoding(); + + expect(workerSpy).toBeCalledTimes(1); + expect(bufferSpy).toBeCalledTimes(1); + }); }); // Blend of different test files @@ -382,5 +440,9 @@ function mockDimensions(clip: VideoClip, width = 540, height = 680) { } function mockSeek(clip: VideoClip) { - return vi.spyOn(clip, 'seek').mockImplementation(async () => { }); + return vi.spyOn(clip.element, 'currentTime', 'set') + .mockImplementation(async function (this: HTMLVideoElement) { + await sleep(1); + this.dispatchEvent(new Event('seeked')); + }); } diff --git a/src/composition/composition.spec.ts b/src/composition/composition.spec.ts index 88e0235..b215599 100644 --- a/src/composition/composition.spec.ts +++ b/src/composition/composition.spec.ts @@ -5,11 +5,15 @@ * Public License, v. 2.0 that can be found in the LICENSE file. */ +import * as PIXI from 'pixi.js'; import { describe, expect, it, beforeEach, vi, afterEach, afterAll, MockInstance } from 'vitest'; import { Composition } from './composition'; -import { Clip, TextClip } from '../clips'; +import { AudioClip, Clip, TextClip } from '../clips'; import { AudioTrack, CaptionTrack, HtmlTrack, ImageTrack, TextTrack, Track, VideoTrack } from '../tracks'; import { Timestamp } from '../models'; +import { sleep } from '../utils'; +import { AudioBufferMock, OfflineAudioContextMock } from '../../vitest.mocks'; +import { AudioSource } from '../sources'; describe('The composition', () => { let composition: Composition; @@ -49,6 +53,17 @@ describe('The composition', () => { expect(composition.playing).toBe(false); }); + it("should trigger an error when the composition can't be initialized", async () => { + const errorFn = vi.fn(); + vi.spyOn(PIXI, 'autoDetectRenderer') + .mockImplementationOnce(() => Promise.reject(new Error('Mocked rejection'))); + const composition = new Composition(); + composition.on('error', errorFn); + expect(errorFn).toBeCalledTimes(0); + await sleep(1); + expect(errorFn).toBeCalledTimes(1); + }) + it('should return width and height', () => { expect(composition.settings.height).toBe(composition.height); expect(composition.settings.width).toBe(composition.width); @@ -83,11 +98,14 @@ describe('The composition', () => { expect(composition.duration.frames).toBe(8 * 30); }); - it('should append canvas to div', () => { + it('should attach a canvas to the dom and be able to remove it', () => { const div = document.createElement('div'); composition.attachPlayer(div); expect(div.children.length).toBe(1); expect(div.children[0] instanceof HTMLCanvasElement).toBe(true); + + composition.detachPlayer(div); + expect(div.children.length).toBe(0); }); it('should append new tracks', () => { @@ -167,6 +185,22 @@ describe('The composition', () => { expect(search4.length).toBe(0); }); + it('should seek a time by timestamp', async () => { + const clip = new Clip({ stop: 15 }); + + const track = composition.createTrack('base'); + await track.add(clip); + + const seekMock = vi.spyOn(track, 'seek'); + computeMock.mockClear(); + + const ts = new Timestamp(400) // 12 frames + composition.seek(ts); + expect(composition.frame).toBe(12); + expect(seekMock).toBeCalledTimes(1); + expect(seekMock.mock.calls[0][0].millis).toBe(400); + }); + it('should render clips when user called play', async () => { const clip = new Clip({ stop: 15 }); @@ -239,9 +273,7 @@ describe('The composition', () => { }); it('should be able to screenshot a frame', async () => { - const clip = new Clip({ stop: 6 * 30 }); - const track = composition.createTrack('base'); - await track.add(clip); + await composition.add(new Clip({ stop: 6 * 30 })); composition.frame = 10; expect(composition.screenshot()).toBe('data:image/png;base64,00'); @@ -249,6 +281,14 @@ describe('The composition', () => { expect(composition.screenshot('jpeg')).toBe('data:image/jpeg;base64,00'); }); + it('should not screenshot a frame when the renderer is undefined', async () => { + await composition.add(new Clip({ stop: 6 * 30 })); + + delete composition.renderer; + + expect(() => composition.screenshot()).toThrowError(); + }); + it('should be able to calculate the correct time', async () => { composition.duration = Timestamp.fromFrames(20 * 30); composition.frame = 10 * 30; @@ -425,3 +465,55 @@ describe('The composition', () => { requestAnimationFrameMock.mockClear(); }); }); + +describe('Composition audio', () => { + vi.stubGlobal('OfflineAudioContext', OfflineAudioContextMock); + + const source = new AudioSource(); + + vi.spyOn(source, 'decode').mockImplementation(async ( + numberOfChannels: number = 2, + sampleRate: number = 48000, + ) => { + const buffer = new AudioBufferMock({ numberOfChannels, sampleRate, length: 8000 }); + + for (let i = 0; i < buffer.channelData.length; i++) { + for (let j = 0; j < buffer.channelData[i].length; j++) { + buffer.channelData[i][j] = 1; + } + } + + return buffer as any; + }); + + vi.spyOn(source, 'createObjectURL').mockImplementationOnce(async () => ''); + + const clip = new AudioClip(source); + + vi.spyOn(clip.element, 'oncanplay', 'set') + .mockImplementation(function (this: HTMLMediaElement, fn) { + fn?.call(this, new Event('canplay')); + }); + + vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(1); + + it('should merge audio clips', async () => { + const composition = new Composition(); + await composition.add(clip.subclip(2, 26)); + + expect(composition.duration.frames).toBe(26); + composition.duration = 30; + expect(composition.duration.frames).toBe(30); + + const buffer = await composition.audio(1, 8000); + expect(buffer.length).toBe(8000); + expect(buffer.numberOfChannels).toBe(1); + expect(buffer.sampleRate).toBe(8000); + + const data = buffer.getChannelData(0); + // audio clip has been trimmed and contains all ones + expect(data.at(0)).toBe(0); + expect(data.at(-1)).toBe(0); + expect(data.at(4000)).toBe(1); + }); +}); diff --git a/src/composition/composition.ts b/src/composition/composition.ts index b8e1128..b1ec22b 100644 --- a/src/composition/composition.ts +++ b/src/composition/composition.ts @@ -370,6 +370,8 @@ export class Composition extends EventEmitterMixin { + if (this.rendering) return; + this.state = 'PLAY'; if (this.frame >= this.duration.frames) { @@ -435,9 +437,7 @@ export class Composition extends EventEmitterMixin new Float32Array(8000)), +} as unknown as AudioBuffer + +describe('The CanvasEncoder', () => { + const canvas = document.createElement('canvas'); + canvas.height = 640; + canvas.width = 480; + + let encoder: CanvasEncoder; + + const configureSpy = vi.spyOn(VideoEncoder.prototype, 'configure').mockImplementation(vi.fn()); + const encodeSpy = vi.spyOn(VideoEncoder.prototype, 'encode').mockImplementation(vi.fn()); + + beforeEach(() => { + encoder = new CanvasEncoder(canvas, { + fps: 60, + gpuBatchSize: 4, + numberOfChannels: 1, + sampleRate: 2000, + videoBitrate: 1e6, + }); + + expect(encoder.fps).toBe(60); + expect(encoder.height).toBe(640); + expect(encoder.width).toBe(480); + expect(encoder.sampleRate).toBe(8000); + expect(encoder.videoBitrate).toBe(1e6); + + configureSpy.mockClear(); + encodeSpy.mockClear(); + }); + + it('should encode video', async () => { + expect(encoder.frame).toBe(0); + expect(configureSpy).toBeCalledTimes(0); + + await encoder.encodeVideo(); + + expect(encoder.frame).toBe(1); + expect(configureSpy).toBeCalledTimes(1); + expect(encodeSpy).toBeCalledTimes(1); + expect(encodeSpy.mock.calls[0][1]?.keyFrame).toBe(true); + + await encoder.encodeVideo(); + + expect(encoder.frame).toBe(2); + expect(configureSpy).toBeCalledTimes(1); + expect(encodeSpy).toBeCalledTimes(2); + expect(encodeSpy.mock.calls[1][1]?.keyFrame).toBe(false); + }); + + it('should encode audio', async () => { + encoder = new CanvasEncoder(canvas, { + numberOfChannels: 1, + sampleRate: 2000, + audio: true, + }); + + expect(encoder.sampleRate).toBe(8000) + + const configureSpy = vi.spyOn(OpusEncoder.prototype, 'configure').mockImplementation(vi.fn()); + const encodeSpy = vi.spyOn(OpusEncoder.prototype, 'encode').mockImplementation(vi.fn()); + + await encoder.encodeAudio(buffer); + + expect(configureSpy).toBeCalledTimes(1); + expect(encodeSpy).toBeCalledTimes(1); + + expect(configureSpy.mock.calls[0][0]).toStrictEqual({ numberOfChannels: 1, sampleRate: 8000 }); + expect(encodeSpy.mock.calls[0][0].data.length).toBe(8000); + expect(encodeSpy.mock.calls[0][0].numberOfFrames).toBe(8000); + }); + + it("should not encode audio when it's not enabled", async () => { + await expect(encoder.encodeAudio(buffer)).rejects.toThrowError(); + }); + + it('should create a blob', async () => { + await encoder.encodeVideo(); + + const blob = await encoder.blob(); + + expect(blob).toBeInstanceOf(Blob); + }); + + it('should not create a blob if the buffer is not defined', async () => { + await expect(() => encoder.blob()).rejects.toThrowError(); + }); +}); diff --git a/src/encoders/encoder.spec.ts b/src/encoders/encoder.spec.ts new file mode 100644 index 0000000..f069ea3 --- /dev/null +++ b/src/encoders/encoder.spec.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Composition } from '../composition'; +import { Encoder } from './encoder'; +import { Clip } from '../clips'; + + +describe('The Encoder', () => { + let composition: Composition; + let encoder: Encoder; + + const configureSpy = vi.spyOn(VideoEncoder.prototype, 'configure').mockImplementation(vi.fn()); + const encodeSpy = vi.spyOn(VideoEncoder.prototype, 'encode').mockImplementation(vi.fn()); + + beforeEach(() => { + composition = new Composition(); + + encoder = new Encoder(composition, { + audio: false, + debug: false, + fps: 60, + gpuBatchSize: 4, + numberOfChannels: 1, + resolution: 0.5, + sampleRate: 8000, + videoBitrate: 1e6, + }); + + configureSpy.mockClear(); + encodeSpy.mockClear(); + + expect(encoder.audio).toBe(false); + expect(encoder.debug).toBe(false); + expect(encoder.fps).toBe(60); + }); + + it('should render the compostion', async () => { + await composition.add(new Clip({ stop: 10 })); + + const pauseSpy = vi.spyOn(composition, 'pause').mockImplementation(async () => undefined); + const seekSpy = vi.spyOn(composition, 'seek').mockImplementation(async () => undefined); + + await encoder.render(); + + expect(pauseSpy).toBeCalledTimes(1); + // before and after + expect(seekSpy).toBeCalledTimes(2); + }); + + it('should not render when the composition renderer is not defined', async () => { + delete composition.renderer; + + await expect(() => encoder.render()).rejects.toThrowError(); + }); + + it('should debug the render process', async () => { + encoder.debug = true; + + await composition.add(new Clip({ stop: 10 })); + + const logSpy = vi.spyOn(console, 'info'); + + await encoder.render(); + + expect(logSpy).toBeCalledTimes(3); + }); +}); diff --git a/src/encoders/encoder.ts b/src/encoders/encoder.ts index 41b4947..2f7b1f3 100644 --- a/src/encoders/encoder.ts +++ b/src/encoders/encoder.ts @@ -50,7 +50,7 @@ export class Encoder extends WebcodecsVideoEncoder { const [videoConfig, audioConfig] = await this.getConfigs(); if (this.debug) { - console.log('Hardware Preference', videoConfig.hardwareAcceleration); + console.info('Hardware Preference', videoConfig.hardwareAcceleration); } const now = performance.now(); diff --git a/src/encoders/opus/opus.utils.spec.ts b/src/encoders/opus/opus.utils.spec.ts new file mode 100644 index 0000000..d2717ef --- /dev/null +++ b/src/encoders/opus/opus.utils.spec.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, expect } from 'vitest'; +import { createOpusHead } from './opus.utils'; + +describe('createOpusHead', () => { + it('should generate a correct Opus header', () => { + const sampleRate = 48000; + const numberOfChannels = 2; + const result = createOpusHead(sampleRate, numberOfChannels); + + // Check that the result is a Uint8Array of length 19 + expect(result).toBeInstanceOf(Uint8Array); + expect(result.length).toBe(19); + + // Check magic signature "OpusHead" + expect(result[0]).toBe('O'.charCodeAt(0)); + expect(result[1]).toBe('p'.charCodeAt(0)); + expect(result[2]).toBe('u'.charCodeAt(0)); + expect(result[3]).toBe('s'.charCodeAt(0)); + expect(result[4]).toBe('H'.charCodeAt(0)); + expect(result[5]).toBe('e'.charCodeAt(0)); + expect(result[6]).toBe('a'.charCodeAt(0)); + expect(result[7]).toBe('d'.charCodeAt(0)); + + // Check version is set to 1 + expect(result[8]).toBe(1); + + // Check number of channels + expect(result[9]).toBe(numberOfChannels); + + // Check pre-skip is 0 (bytes 10 and 11) + expect(result[10]).toBe(0); + expect(result[11]).toBe(0); + + // Check sample rate is correctly encoded (bytes 12-15) + expect(result[12]).toBe(sampleRate & 0xFF); + expect(result[13]).toBe((sampleRate >> 8) & 0xFF); + expect(result[14]).toBe((sampleRate >> 16) & 0xFF); + expect(result[15]).toBe((sampleRate >> 24) & 0xFF); + + // Check gain is 0 (bytes 16 and 17) + expect(result[16]).toBe(0); + expect(result[17]).toBe(0); + + // Check channel mapping is 0 + expect(result[18]).toBe(0); + }); + + it('should correctly encode a different sample rate and number of channels', () => { + const sampleRate = 44100; + const numberOfChannels = 1; + const result = createOpusHead(sampleRate, numberOfChannels); + + // Check number of channels + expect(result[9]).toBe(numberOfChannels); + + // Check sample rate is correctly encoded (bytes 12-15) + expect(result[12]).toBe(sampleRate & 0xFF); + expect(result[13]).toBe((sampleRate >> 8) & 0xFF); + expect(result[14]).toBe((sampleRate >> 16) & 0xFF); + expect(result[15]).toBe((sampleRate >> 24) & 0xFF); + }); +}); diff --git a/src/encoders/utils.spec.ts b/src/encoders/utils.spec.ts new file mode 100644 index 0000000..25e867e --- /dev/null +++ b/src/encoders/utils.spec.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, expect, it, vi } from 'vitest'; + +import { audioClipFilter, createRenderEventDetail, createStreamTarget, toOpusSampleRate, withError } from './utils'; +import { ArrayBufferTarget, FileSystemWritableFileStreamTarget } from 'mp4-muxer'; +import { Clip, MediaClip } from '../clips'; + + +describe('createStreamTarget', () => { + it('should create a downloadable stream target', async () => { + const res = await createStreamTarget('test.mp4'); + + expect(res.target).toBeInstanceOf(ArrayBufferTarget); + expect(res.fastStart).toBe('in-memory'); + + const a = document.createElement('a'); + + const clickSpy = vi.spyOn(a, 'click'); + const createSpy = vi.spyOn(document, 'createElement').mockReturnValue(a); + + await res.close(true); + + expect(clickSpy).toBeCalledTimes(1); + expect(createSpy).toBeCalledTimes(1); + expect(a.download).toBe('test.mp4'); + }); + + it('should upload the file if the target is an http address', async () => { + const res = await createStreamTarget('https://s3.com/test.mp4'); + + expect(res.target).toBeInstanceOf(ArrayBufferTarget); + expect(res.fastStart).toBe('in-memory'); + + const fetchSpy = vi.spyOn(global, 'fetch'); + + await res.close(true); + + expect(fetchSpy).toBeCalledTimes(1); + expect(fetchSpy.mock.calls[0][0]).toBe('https://s3.com/test.mp4'); + expect(fetchSpy.mock.calls[0][1]?.method).toBe('PUT'); + }); + + it('should throw an error when using http upload and the response is not ok', async () => { + const res = await createStreamTarget('https://s3.com/test.mp4'); + + const fetchSpy = vi.spyOn(global, 'fetch').mockReturnValue({ + ok: false, + } as any) + + await expect(() => res.close(true)).rejects.toThrowError(); + + fetchSpy.mockRestore(); + }); + + it('should handle the file system access', async () => { + const handle = new FileSystemFileHandle(); + const res = await createStreamTarget(handle); + + expect(res.target).toBeInstanceOf(FileSystemWritableFileStreamTarget); + expect(res.fastStart).toBe(false); + + const closeSpy = vi.spyOn(FileSystemWritableFileStream.prototype, 'close'); + + await res.close(true); + + expect(closeSpy).toBeCalledTimes(1); + }); +}); + +describe('withError', () => { + it('should reject promise all errors', async () => { + const promise1 = Promise.resolve(3); + const promise2 = new Promise((_, reject) => + setTimeout(reject, 100, 'foo'), + ); + + const promise = withError(Promise.allSettled([promise1, promise2])); + + await expect(promise).rejects.toThrowError(); + }) +}); + +describe('createRenderEventDetail', () => { + it('should should calculate the remaining time', async () => { + const date = new Date(); + date.setSeconds(date.getSeconds() - 10); + const detail = createRenderEventDetail(50, 100, date.getTime()); + expect(detail.progress).toBe(50); + expect(detail.total).toBe(100); + // it took 10 secs for 50% there should be 10 seconds remaining + expect(detail.remaining.getSeconds()).toBe(10); + }); +}); + +describe('audioClipFilter', () => { + it('should filter clips', async () => { + const clips = [new Clip(), new Clip(), new MediaClip({ disabled: true }), new MediaClip()]; + + expect(clips.filter(audioClipFilter).length).toBe(1); + }); +}); + +describe('toOpusSampleRate', () => { + it('should find the closes available opus sample rate', async () => { + expect(toOpusSampleRate(0)).toBe(8000); + expect(toOpusSampleRate(10000)).toBe(8000); + expect(toOpusSampleRate(10001)).toBe(12000); + expect(toOpusSampleRate(50000)).toBe(48000); + }); +}); diff --git a/src/encoders/webassembly.audio.spec.ts b/src/encoders/webassembly.audio.spec.ts new file mode 100644 index 0000000..e092b47 --- /dev/null +++ b/src/encoders/webassembly.audio.spec.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AudioBufferMock } from '../../vitest.mocks'; +import { Composition } from '../composition'; +import { WebassemblyAudioEncoder } from './webassembly.audio'; +import { Muxer, ArrayBufferTarget } from 'mp4-muxer'; +import { OpusEncoder } from './opus'; + +const config = { + codec: 'opus', + numberOfChannels: 2, + sampleRate: 48000, +} as const; + +describe('WebassemblyAudioEncoder', () => { + const composition = new Composition(); + + const bufferSpy = vi.spyOn(composition, 'audio').mockImplementation( + async (numberOfChannels: number = 1, sampleRate: number = 1e6) => + new AudioBufferMock({ sampleRate, numberOfChannels, length: 50 }) as AudioBuffer + ); + const configureSpy = vi.spyOn(OpusEncoder.prototype, 'configure').mockImplementation(vi.fn()); + const encodeSpy = vi.spyOn(OpusEncoder.prototype, 'encode') + .mockImplementation(function (this: OpusEncoder, ...args: any[]) { + this.output({ + data: new Uint8Array(10), + duration: 20, + timestamp: 0, + type: 'key' + }, { decoderConfig: config }) + return vi.fn()(...args); + }); + + const encoder = new WebassemblyAudioEncoder(composition); + + const muxer = new Muxer({ + target: new ArrayBufferTarget(), + audio: config, + fastStart: 'in-memory' + }); + + const muxSpy = vi.spyOn(muxer, 'addAudioChunkRaw'); + + beforeEach(() => { + bufferSpy.mockClear(); + configureSpy.mockClear(); + encodeSpy.mockClear(); + muxSpy.mockClear(); + }) + + it('should encode the audio of the composition using the provided configuration', async () => { + await encoder.encode(muxer, { ...config, sampleRate: 50_000 }); + + expect(bufferSpy).toHaveBeenCalledTimes(1); + expect(configureSpy).toHaveBeenCalledWith(config); + expect(encodeSpy.mock.calls[0][0].numberOfFrames).toBe(50); + // 2 channels times 50 + expect(encodeSpy.mock.calls[0][0].data.length).toBe(100); + + expect(muxSpy).toHaveBeenCalledTimes(1); + expect(muxSpy.mock.calls[0][0]).toBeInstanceOf(Uint8Array); + expect(muxSpy.mock.calls[0][0].length).toBe(10); + expect(muxSpy.mock.calls[0][1]).toBe('key'); + expect(muxSpy.mock.calls[0][2]).toBe(0); + expect(muxSpy.mock.calls[0][3]).toBe(20); + }); +}); diff --git a/src/encoders/webassembly.audio.ts b/src/encoders/webassembly.audio.ts index a5d9563..20821bb 100644 --- a/src/encoders/webassembly.audio.ts +++ b/src/encoders/webassembly.audio.ts @@ -42,7 +42,7 @@ export class WebassemblyAudioEncoder implements WebAudioEncoder { error: console.error, }); - await encoder.configure(config); + await encoder.configure({ ...config, sampleRate }); encoder.encode({ data: bufferToI16Interleaved(output), diff --git a/src/encoders/webcodecs.audio.spec.ts b/src/encoders/webcodecs.audio.spec.ts new file mode 100644 index 0000000..5ca204f --- /dev/null +++ b/src/encoders/webcodecs.audio.spec.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AudioBufferMock } from '../../vitest.mocks'; +import { Composition } from '../composition'; +import { WebcodecsAudioEncoder } from './webcodecs.audio'; +import { Muxer, ArrayBufferTarget } from 'mp4-muxer'; + +const config = { + codec: 'opus', + numberOfChannels: 2, + sampleRate: 48000, +} as const; + +describe('WebcodecsAudioEncoder', () => { + const composition = new Composition(); + + const bufferSpy = vi.spyOn(composition, 'audio').mockImplementation( + async (numberOfChannels: number = 1, sampleRate: number = 1e6) => + new AudioBufferMock({ sampleRate, numberOfChannels, length: 50 }) as AudioBuffer + ); + const configureSpy = vi.spyOn(AudioEncoder.prototype, 'configure').mockImplementation(vi.fn()); + const encodeSpy = vi.spyOn(AudioEncoder.prototype, 'encode').mockImplementation(vi.fn()); + + const encoder = new WebcodecsAudioEncoder(composition); + + const muxer = new Muxer({ + target: new ArrayBufferTarget(), + audio: config, + fastStart: 'in-memory' + }); + + const muxSpy = vi.spyOn(muxer, 'addAudioChunk'); + + beforeEach(() => { + bufferSpy.mockClear(); + configureSpy.mockClear(); + encodeSpy.mockClear(); + muxSpy.mockClear(); + }) + + it('should encode the audio of the composition using the provided configuration', async () => { + await encoder.encode(muxer, config); + + expect(bufferSpy).toHaveBeenCalledTimes(1); + expect(configureSpy).toHaveBeenCalledWith(config); + expect(encodeSpy.mock.calls[0][0]).toBeInstanceOf(AudioData); + }); +}); diff --git a/src/encoders/webcodecs.video.spec.ts b/src/encoders/webcodecs.video.spec.ts new file mode 100644 index 0000000..86b6459 --- /dev/null +++ b/src/encoders/webcodecs.video.spec.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Composition } from '../composition'; +import { Muxer, ArrayBufferTarget } from 'mp4-muxer'; +import { WebcodecsVideoEncoder } from './webcodecs.video'; +import { Clip } from '../clips'; + +const config = { + codec: 'avc', + width: 1280, + height: 720 +} as const; + +describe('WebcodecsVideoEncoder', () => { + let composition: Composition; + let encoder: WebcodecsVideoEncoder; + + const configureSpy = vi.spyOn(VideoEncoder.prototype, 'configure').mockImplementation(vi.fn()); + const encodeSpy = vi.spyOn(VideoEncoder.prototype, 'encode').mockImplementation(vi.fn()); + + const muxer = new Muxer({ + target: new ArrayBufferTarget(), + video: config, + fastStart: 'in-memory' + }); + + const muxSpy = vi.spyOn(muxer, 'addVideoChunk'); + + beforeEach(() => { + composition = new Composition(); + + encoder = new WebcodecsVideoEncoder(composition, { + audio: false, + debug: false, + fps: 60, + gpuBatchSize: 4, + numberOfChannels: 1, + resolution: 0.5, + sampleRate: 8000, + videoBitrate: 1e6, + }); + + configureSpy.mockClear(); + encodeSpy.mockClear(); + muxSpy.mockClear(); + + expect(encoder.audio).toBe(false); + expect(encoder.debug).toBe(false); + expect(encoder.fps).toBe(60); + expect(encoder.gpuBatchSize).toBe(4); + expect(encoder.numberOfChannels).toBe(1); + expect(encoder.resolution).toBe(0.5); + expect(encoder.sampleRate).toBe(8000); + expect(encoder.videoBitrate).toBe(1e6); + }) + + it('should encode the frames of the composition using the provided configuration', async () => { + const seekSpy = vi.spyOn(composition, 'seek').mockImplementation(vi.fn()); + + await composition.add(new Clip({ stop: 10 })); + + await encoder.encodeVideo(muxer, config); + + expect(seekSpy).toBeCalledTimes(1); + expect(encodeSpy).toBeCalledTimes(20); + }); + + it('should throw a encoder error if the renderer is not defined', async () => { + delete composition.renderer; + + await expect(() => encoder.encodeVideo(muxer, config)).rejects.toThrowError(); + }); + + it('should throw a dom exception if the rendering has been aborted', async () => { + await composition.add(new Clip({ stop: 10 })); + + const controller = new AbortController(); + + controller.abort(); + + await expect(() => encoder.encodeVideo(muxer, config, controller.signal)).rejects.toThrow(DOMException); + expect(encodeSpy).toBeCalledTimes(0); + }); + + it('should debug the encoding process', async () => { + const consoleSpy = vi.spyOn(console, 'info'); + await composition.add(new Clip({ stop: 10 })); + + await encoder.encodeVideo(muxer, config); + + expect(consoleSpy).toBeCalledTimes(0); + + encoder.debug = true; + + await encoder.encodeVideo(muxer, config); + + expect(consoleSpy).toBeCalledTimes(1); + }); +}); diff --git a/src/encoders/webcodecs.video.ts b/src/encoders/webcodecs.video.ts index 41c838b..7eaa260 100644 --- a/src/encoders/webcodecs.video.ts +++ b/src/encoders/webcodecs.video.ts @@ -46,7 +46,7 @@ export class WebcodecsVideoEncoder extends EventEmitter() impleme /** * render and encode visual frames */ - protected async encodeVideo(muxer: Muxer, config: VideoEncoderConfig, signal?: AbortSignal) { + public async encodeVideo(muxer: Muxer, config: VideoEncoderConfig, signal?: AbortSignal) { const { renderer, tracks, duration } = this.composition; const totalFrames = Math.floor(duration.seconds * this.fps); diff --git a/src/models/animation-builder.spec.ts b/src/models/animation-builder.spec.ts index 4f65d79..0d91f20 100644 --- a/src/models/animation-builder.spec.ts +++ b/src/models/animation-builder.spec.ts @@ -118,4 +118,8 @@ describe('The Animation Builder', () => { expect(width.input[0]).toBe(12 / 30 * 1000); expect(width.output[0]).toBe(20); }); + + it("should not create an animation if no property has been called first", () => { + expect(() => animate.to(20, 12)).toThrowError(); + }); }); diff --git a/src/models/transcript.spec.ts b/src/models/transcript.spec.ts index 7d5473a..9036d4f 100644 --- a/src/models/transcript.spec.ts +++ b/src/models/transcript.spec.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from 'vitest'; import { Transcript, Word, WordGroup } from '../models'; +import { setFetchMockReturnValue } from '../../vitest.mocks'; describe('Transcript tests', () => { it('the word should calculate the duration correctly', () => { @@ -211,4 +212,85 @@ describe('Transcript tests', () => { expect(subset.groups.at(0)?.words.at(-1)?.start.seconds).toBe(10); expect(subset.groups.at(0)?.words.at(-1)?.stop.seconds).toBe(13); }); + + it('should optimize the word timestamps', () => { + const transcript = new Transcript([ + new WordGroup([new Word('Lorem', 0, 12), new Word('Ipsum', 15, 21)]), + new WordGroup([new Word('is', 18, 27)]), + ]); + + transcript.optimize(); + + expect(transcript.groups.length).toBe(2); + expect(transcript.groups[0].words[0].start.millis).toBe(0); + expect(transcript.groups[0].words[0].stop.millis).toBe(14); + + expect(transcript.groups[0].words[1].start.millis).toBe(15); + expect(transcript.groups[0].words[1].stop.millis).toBe(21); + + expect(transcript.groups[1].words[0].start.millis).toBe(22); + expect(transcript.groups[1].words[0].stop.millis).toBe(27); + }); + + it('should be converatble to an srt', () => { + const transcript = new Transcript([ + new WordGroup([new Word('Lorem', 0, 1e3), new Word('Ipsum', 2e3, 5e3)]), + new WordGroup([new Word('is', 7e3, 8e3)]), + ]); + + const { text, blob } = transcript.toSRT({ count: [2] }); + + expect(text).toContain(`1 +00:00:00,000 --> 00:00:05,000 +Lorem Ipsum` + ); + + expect(text).toContain(`2 +00:00:07,000 --> 00:00:08,000 +is` + ); + + expect(blob.type).toBe('text/plain;charset=utf8'); + }); + + it('should be able to instantiate from an url', async () => { + const resetFetch = setFetchMockReturnValue({ + ok: true, + json: async () => ([ + [ + { token: 'Lorem', start: 0, stop: 12 }, + { token: 'Ipsum', start: 15, stop: 20 }, + ], + [ + { token: 'is', start: 21, stop: 38 }, + ] + ]), + }); + + const transcript = await Transcript.from('http://diffusion.mov/caption.json'); + + + expect(transcript.groups.length).toBe(2); + expect(transcript.groups[0].words[0].start.millis).toBe(0); + expect(transcript.groups[0].words[0].stop.millis).toBe(12); + expect(transcript.groups[0].words[0].text).toBe('Lorem'); + + expect(transcript.groups[0].words[1].start.millis).toBe(15); + expect(transcript.groups[0].words[1].stop.millis).toBe(20); + expect(transcript.groups[0].words[1].text).toBe('Ipsum'); + + expect(transcript.groups[1].words[0].start.millis).toBe(21); + expect(transcript.groups[1].words[0].stop.millis).toBe(38); + expect(transcript.groups[1].words[0].text).toBe('is'); + + resetFetch(); + }); + + it('should not be able to instantiate when the response is not ok', async () => { + const resetFetch = setFetchMockReturnValue({ ok: false, }); + + await expect(() => Transcript.from('http://diffusion.mov/caption.json')).rejects.toThrowError(); + + resetFetch(); + }); }); diff --git a/src/models/transcript.ts b/src/models/transcript.ts index 8719ad2..739010a 100644 --- a/src/models/transcript.ts +++ b/src/models/transcript.ts @@ -88,7 +88,7 @@ export class Transcript implements Serializer { * Convert the transcript into a SRT compatible * string and downloadable blob */ - public toSRT(options: GeneratorOptions): { text: string; blob: Blob } { + public toSRT(options: GeneratorOptions = {}): { text: string; blob: Blob } { let idx: number = 1; let srt: string = ''; diff --git a/src/services/thread.spec.ts b/src/services/thread.spec.ts new file mode 100644 index 0000000..d8b9a3d --- /dev/null +++ b/src/services/thread.spec.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, expect, it, vi } from 'vitest'; +import { Thread, withThreadErrorHandler } from './thread'; + +// Mocking the Worker class to simulate a Web Worker +class MockWorker { + onmessage: ((event: MessageEvent) => void) | null = null; + onerror: ((event: ErrorEvent) => void) | null = null; + terminated = false; + + postMessage = vi.fn(); + + addEventListener(event: string, handler: (event: MessageEvent) => void) { + if (event === 'message') { + this.onmessage = handler; + } + } + + terminate() { + this.terminated = true; + } + + // Simulate receiving a message + simulateMessage(data: any) { + if (this.onmessage) { + this.onmessage({ data } as MessageEvent); + } + } + + // Simulate an error + simulateError(message: string) { + if (this.onerror) { + this.onerror(new ErrorEvent('error', { message })); + } + } +} + +describe('Thread', () => { + it('should post a message and receive a result', async () => { + const mockWorkerConstructor = vi.fn(() => new MockWorker()); + const thread = new Thread(mockWorkerConstructor as any); + + const resultPromise = thread.run<{ input: number }>({ input: 42 }); + + // Simulate the worker responding with a result + const mockWorkerInstance = mockWorkerConstructor.mock.results[0].value; + mockWorkerInstance.simulateMessage({ type: 'result', result: 84 }); + + const response = await resultPromise; + + expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith({ type: 'init', input: 42 }); + + // Adjust assertion to handle the structure returned by the `Thread.run()` method + expect(response).toEqual({ result: { result: 84, type: undefined }, error: undefined }); + + expect(mockWorkerInstance.terminated).toBe(true); + }); + + it('should handle worker errors and return error', async () => { + const mockWorkerConstructor = vi.fn(() => new MockWorker()); + const thread = new Thread(mockWorkerConstructor as any); + + const resultPromise = thread.run(); + + // Simulate the worker responding with an error + const mockWorkerInstance = mockWorkerConstructor.mock.results[0].value; + mockWorkerInstance.simulateMessage({ type: 'error', message: 'Something went wrong' }); + + const response = await resultPromise; + + expect(response).toEqual({ result: undefined, error: 'Something went wrong' }); + expect(mockWorkerInstance.terminated).toBe(true); + }); + + it('should call the event listener if provided', async () => { + const mockWorkerConstructor = vi.fn(() => new MockWorker()); + const thread = new Thread(mockWorkerConstructor as any); + + const eventListener = vi.fn(); + const resultPromise = thread.run(undefined, eventListener); + + // Simulate the worker responding with a result + const mockWorkerInstance = mockWorkerConstructor.mock.results[0].value; + mockWorkerInstance.simulateMessage({ type: 'result', result: 84 }); + + await resultPromise; + + // Adjust assertion to account for the fact that `type` is set to undefined + expect(eventListener).toHaveBeenCalledWith({ result: 84, type: undefined }); + }); + + it('should terminate the worker even on error', async () => { + const mockWorkerConstructor = vi.fn(() => new MockWorker()); + const thread = new Thread(mockWorkerConstructor as any); + + const resultPromise = thread.run(); + + // Simulate the worker responding with an error + const mockWorkerInstance = mockWorkerConstructor.mock.results[0].value; + + // Simulate a message with an error type + mockWorkerInstance.simulateMessage({ type: 'error', message: 'Worker failed' }); + + const response = await resultPromise; + + expect(response).toEqual({ result: undefined, error: 'Worker failed' }); + expect(mockWorkerInstance.terminated).toBe(true); + }); +}); + +describe('withThreadErrorHandler', () => { + it('should call the main function and post an error on failure', async () => { + const mockPostMessage = vi.fn(); + const mainFunction = vi.fn().mockRejectedValue(new Error('Test error')); + + // Mock the global `self` object to intercept postMessage calls + globalThis.self = { postMessage: mockPostMessage } as any; + + const handler = withThreadErrorHandler(mainFunction); + + const event = { data: 'test-event' } as MessageEvent; + await handler(event); + + expect(mainFunction).toHaveBeenCalledWith(event); + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'error', + message: 'Test error', + }); + }); + + it('should post a default error message if no error message is provided', async () => { + const mockPostMessage = vi.fn(); + const mainFunction = vi.fn().mockRejectedValue({}); + + // Mock the global `self` object to intercept postMessage calls + globalThis.self = { postMessage: mockPostMessage } as any; + + const handler = withThreadErrorHandler(mainFunction); + + const event = { data: 'test-event' } as MessageEvent; + await handler(event); + + expect(mainFunction).toHaveBeenCalledWith(event); + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'error', + message: 'An unkown worker error occured', + }); + }); + + it('should not post an error if the main function succeeds', async () => { + const mockPostMessage = vi.fn(); + const mainFunction = vi.fn().mockResolvedValue(undefined); + + // Mock the global `self` object to intercept postMessage calls + globalThis.self = { postMessage: mockPostMessage } as any; + + const handler = withThreadErrorHandler(mainFunction); + + const event = { data: 'test-event' } as MessageEvent; + await handler(event); + + expect(mainFunction).toHaveBeenCalledWith(event); + expect(mockPostMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/sources/audio.spec.ts b/src/sources/audio.spec.ts new file mode 100644 index 0000000..adff8af --- /dev/null +++ b/src/sources/audio.spec.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, it, vi, beforeEach, expect } from 'vitest'; +import { AudioSource } from './audio'; // Import the AudioSource class + +// Mocking the OfflineAudioContext class +class MockOfflineAudioContext { + constructor(public numberOfChannels: number, public length: number, public sampleRate: number) { } + + decodeAudioData(_: ArrayBuffer): Promise { + const audioBuffer = { + duration: 5, // Mock duration + sampleRate: this.sampleRate, + getChannelData: () => new Float32Array(5000), // Return a dummy Float32Array + } as any as AudioBuffer; + return Promise.resolve(audioBuffer); + } +} + +vi.stubGlobal('OfflineAudioContext', MockOfflineAudioContext); // Stub the global OfflineAudioContext + +describe('AudioSource', () => { + let audioSource: AudioSource; + + beforeEach(() => { + audioSource = new AudioSource(); + audioSource.file = new File([], 'audio.mp3', { type: 'audio/mp3' }); + }); + + it('should decode an audio buffer correctly', async () => { + const buffer = await audioSource.decode(2, 44100); + expect(buffer.duration).toBe(5); // Mock duration + expect(buffer.sampleRate).toBe(44100); + expect(audioSource.audioBuffer).toBe(buffer); + expect(audioSource.duration.seconds).toBe(5); // Ensure duration is set + }); + + it('should create a thumbnail with correct DOM elements', async () => { + const thumbnail = await audioSource.thumbnail(60, 50, 0); + + // Check if the thumbnail is a div + expect(thumbnail.tagName).toBe('DIV'); + expect(thumbnail.className).toContain('audio-samples'); + + // Check if it has the right number of children + expect(thumbnail.children.length).toBe(60); + + // Check if each child has the correct class + for (const child of thumbnail.children) { + expect(child.className).toContain('audio-sample-item'); + } + }); +}); diff --git a/src/sources/audio.ts b/src/sources/audio.ts index 4968567..eb8bbb7 100644 --- a/src/sources/audio.ts +++ b/src/sources/audio.ts @@ -11,12 +11,12 @@ import type { ClipType } from '../clips'; import type { ArgumentTypes } from '../types'; export class AudioSource extends Source { - public readonly type: ClipType = 'base'; + public readonly type: ClipType = 'audio'; public audioBuffer?: AudioBuffer; public async decode( numberOfChannels: number = 2, - sampleRate: number = 44_100, + sampleRate: number = 48000, ): Promise { const buffer = await this.arrayBuffer(); diff --git a/src/sources/html.spec.ts b/src/sources/html.spec.ts index c8cca5f..d12419d 100644 --- a/src/sources/html.spec.ts +++ b/src/sources/html.spec.ts @@ -7,6 +7,8 @@ import { describe, expect, it, vi } from 'vitest'; import { HtmlSource } from './html'; +import { setFetchMockReturnValue } from '../../vitest.mocks'; +import { sleep } from '../utils'; describe('The Html Source Object', () => { it('should create an object url when the iframe loads', async () => { @@ -43,6 +45,52 @@ describe('The Html Source Object', () => { await expect(() => source.from(file)).rejects.toThrowError(); expect(evtMock).toHaveBeenCalledTimes(1); }); + + it('should have a valid document getter', async () => { + const source = new HtmlSource(); + + expect(source.document).toBeTruthy(); + }); + + it('should create an object url after the fetch has been completed', async () => { + const resetFetch = setFetchMockReturnValue({ + ok: true, + blob: async () => { + await sleep(10); + return new Blob([], { type: 'text/html' }); + }, + }); + + const source = new HtmlSource(); + + mockIframeValid(source); + + source.from('https://external.html'); + + expect(source.objectURL).toBeUndefined(); + + const url = await source.createObjectURL(); + + expect(url).toBe("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E"); + + expect(source.objectURL).toBeDefined(); + + resetFetch(); + }); + + it('should retrun a video as thumbnail', async () => { + const file = new File([], 'test.html', { type: 'text/html' }); + const source = new HtmlSource(); + + mockIframeValid(source); + mockDocumentValid(source); + + await source.from(file); + + const thumbnail = await source.thumbnail(); + + expect(thumbnail).toBeInstanceOf(Image); + }); }); function mockIframeValid(source: HtmlSource) { diff --git a/src/sources/html.ts b/src/sources/html.ts index 29f3a96..04d8580 100644 --- a/src/sources/html.ts +++ b/src/sources/html.ts @@ -11,7 +11,7 @@ import { documentToSvgImageUrl } from './html.utils'; import type { ClipType } from '../clips'; export class HtmlSource extends Source { - public readonly type: ClipType = 'base'; + public readonly type: ClipType = 'html'; /** * Access to the iframe that is required * for extracting the html's dimensions diff --git a/src/sources/html.utils.spec.ts b/src/sources/html.utils.spec.ts new file mode 100644 index 0000000..0c9858b --- /dev/null +++ b/src/sources/html.utils.spec.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2024 The Diffusion Studio Authors + * + * This Source Code Form is subject to the terms of the Mozilla + * Public License, v. 2.0 that can be found in the LICENSE file. + */ + +import { describe, expect, it, vi } from 'vitest'; +import { documentToSvgImageUrl, fontToBas64Url } from './html.utils'; +import { setFetchMockReturnValue } from '../../vitest.mocks'; + +describe('documentToSvgImageUrl', () => { + it('should return empty SVG if document is not provided', () => { + const result = documentToSvgImageUrl(); + expect(result).toBe("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E"); + }); + + it('should return empty SVG if document has no body', () => { + const mockDocument = document.implementation.createDocument('', '', null); + const result = documentToSvgImageUrl(mockDocument); + expect(result).toBe("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E"); + }); + + it('should return valid SVG when document has body and style', () => { + const mockDocument = document.implementation.createHTMLDocument('Test Document'); + const body = mockDocument.body; + const style = mockDocument.createElement('style'); + style.textContent = 'body { background: red; }'; + mockDocument.head.appendChild(style); + body.innerHTML = '
Hello World
'; + + const result = documentToSvgImageUrl(mockDocument); + + // Check if result starts with valid data URI and contains parts of the body and style + expect(result).toContain('data:image/svg+xml;base64,'); + const decodedSvg = atob(result.split(',')[1]); + expect(decodedSvg).toContain('Hello World'); + expect(decodedSvg).toContain('body { background: red; }'); + }); + + it('should return valid SVG when document has body but no style', () => { + const mockDocument = document.implementation.createHTMLDocument('Test Document'); + const body = mockDocument.body; + body.innerHTML = '
Hello World
'; + + const result = documentToSvgImageUrl(mockDocument); + + // Check if result starts with valid data URI and contains parts of the body + expect(result).toContain('data:image/svg+xml;base64,'); + const decodedSvg = atob(result.split(',')[1]); + expect(decodedSvg).toContain('Hello World'); + expect(decodedSvg).not.toContain('