From bc486dfc44eb227352154d15374d1dc21aa0abc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:40:03 +0800 Subject: [PATCH 01/36] build(deps-dev): bump minimatch from 3.1.2 to 3.1.5 (#1791) Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6edf68df3..71c056823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4604,9 +4604,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -9181,9 +9181,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" From fdc477194e8915436b84e9d8c2893a8e30dc5267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 01:08:21 +0800 Subject: [PATCH 02/36] build(deps-dev): bump @babel/plugin-transform-modules-systemjs (#1809) Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.16.7 to 7.29.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs) --- updated-dependencies: - dependency-name: "@babel/plugin-transform-modules-systemjs" dependency-version: 7.29.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 558 +++++++++++++++------------------------------- 1 file changed, 179 insertions(+), 379 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71c056823..977a55bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,13 +43,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -95,15 +96,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -242,14 +244,11 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, "engines": { "node": ">=6.9.0" } @@ -267,34 +266,33 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "dependencies": { - "@babel/types": "^7.16.7" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { @@ -310,9 +308,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "engines": { "node": ">=6.9.0" @@ -385,18 +383,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -440,25 +438,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.29.0" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -1243,16 +1230,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz", - "integrity": "sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1743,49 +1729,45 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1885,17 +1867,13 @@ "dev": true }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1907,25 +1885,16 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2323,18 +2292,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2574,41 +2531,6 @@ } ] }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -2657,21 +2579,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", @@ -2956,15 +2863,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { "version": "8.24.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", @@ -4327,15 +4225,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { @@ -5565,15 +5463,6 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5945,13 +5834,14 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" } }, "@babel/compat-data": { @@ -5984,15 +5874,16 @@ } }, "@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "requires": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" } }, "@babel/helper-annotate-as-pure": { @@ -6092,14 +5983,11 @@ "@babel/types": "^7.23.0" } }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true }, "@babel/helper-member-expression-to-functions": { "version": "7.17.7", @@ -6111,28 +5999,24 @@ } }, "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" } }, "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" } }, "@babel/helper-optimise-call-expression": { @@ -6145,9 +6029,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true }, "@babel/helper-remap-async-to-generator": { @@ -6202,15 +6086,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true }, "@babel/helper-validator-option": { @@ -6242,23 +6126,15 @@ "@babel/types": "^7.17.0" } }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.29.0" } }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", @@ -6763,16 +6639,15 @@ } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz", - "integrity": "sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" } }, "@babel/plugin-transform-modules-umd": { @@ -7107,43 +6982,39 @@ } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" } }, "@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" } }, "@eslint/eslintrc": { @@ -7210,14 +7081,13 @@ "dev": true }, "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "@jridgewell/resolve-uri": { @@ -7226,22 +7096,16 @@ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", @@ -7505,15 +7369,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -7666,34 +7521,6 @@ "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7730,21 +7557,6 @@ "wrap-ansi": "^7.0.0" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", @@ -7972,12 +7784,6 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "eslint": { "version": "8.24.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", @@ -8973,9 +8779,9 @@ } }, "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true }, "json-parse-even-better-errors": { @@ -9874,12 +9680,6 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", From 7f851d575d9ab0d383ea46b293a70d0fddefca94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 01:28:56 +0800 Subject: [PATCH 03/36] build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#1773) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 977a55bf1..524b7d7bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4213,9 +4213,9 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "dependencies": { "argparse": "^2.0.1" @@ -8770,9 +8770,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" From 3655418663542d706b1b18c3de21b72a2f7198d3 Mon Sep 17 00:00:00 2001 From: naveenkirugulige Date: Mon, 11 May 2026 13:01:44 +0530 Subject: [PATCH 04/36] fix: preserve facing mode in applyConstraints --- src/MediaStreamTrack.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/MediaStreamTrack.ts b/src/MediaStreamTrack.ts index fdfb0c6e0..298327161 100644 --- a/src/MediaStreamTrack.ts +++ b/src/MediaStreamTrack.ts @@ -210,6 +210,11 @@ export default class MediaStreamTrack extends EventTarget Date: Tue, 26 May 2026 20:11:48 +0900 Subject: [PATCH 05/36] ci: pinact (#84) --- .github/workflows/pinact_update.yaml | 58 ++++++++++++++++++++++++++++ aqua.yaml | 6 +++ 2 files changed, 64 insertions(+) create mode 100644 .github/workflows/pinact_update.yaml create mode 100644 aqua.yaml diff --git a/.github/workflows/pinact_update.yaml b/.github/workflows/pinact_update.yaml new file mode 100644 index 000000000..7f848d7a7 --- /dev/null +++ b/.github/workflows/pinact_update.yaml @@ -0,0 +1,58 @@ +name: Pinact update + +on: + push: + branches: + - main + paths: + - ".github/workflows/**" + schedule: + # Weekly on Monday at 00:00 UTC + - cron: "0 0 * * 1" + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write + +jobs: + pinact: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install aqua (installs pinact) + uses: aquaproj/aqua-installer@11dd79b4e498d471a9385aa9fb7f62bb5f52a73c # v4.0.4 + with: + aqua_version: v2.56.1 + aqua_opts: "-c aqua.yaml" + + - name: Run pinact (fix workflow files) + shell: bash + run: | + set -euo pipefail + + # Keep the job going even if pinact exits non-zero due to changes. + set +e + aqua exec -- pinact run \ + --check \ + --diff \ + --fix \ + --update \ + --min-age "30" + exit_code="$?" + set -e + + echo "pinact exit code: ${exit_code}" + + - name: Create PR (if changes) + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.PINACT_GITHUB_TOKEN }} + commit-message: "chore(pinact): pin/update GitHub Actions" + title: "chore(pinact): pin/update GitHub Actions" + body: | + Automated update by pinact. + branch: "ci/pinact_action" + delete-branch: true diff --git a/aqua.yaml b/aqua.yaml new file mode 100644 index 000000000..b702052d5 --- /dev/null +++ b/aqua.yaml @@ -0,0 +1,6 @@ +registries: + - type: standard + ref: v4.400.0 + +packages: + - name: suzuki-shunsuke/pinact@v3.9.2 From bfb1b732ede2e3b6bbcfd2b961b88137f2aad19d Mon Sep 17 00:00:00 2001 From: davidliu Date: Tue, 26 May 2026 23:11:16 +0900 Subject: [PATCH 06/36] chore(pinact): pin/update GitHub Actions (#85) Automated update by pinact. Co-authored-by: davidliu <242400+davidliu@users.noreply.github.com> --- .github/workflows/android_ci.yml | 6 +++--- .github/workflows/ci.yml | 8 ++++---- .github/workflows/ios_ci.yml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 45147f198..5451e1b9f 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -13,12 +13,12 @@ jobs: android-compile: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' java-version: '17' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31618c5cf..4fc007920 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' cache: 'npm' @@ -27,8 +27,8 @@ jobs: - 'ios' - 'android' steps: - - uses: actions/checkout@v4 - - uses: jidicula/clang-format-action@v4.15.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: jidicula/clang-format-action@654a770daa28443dd111d133e4083e21c1075674 # v4.18.0 with: clang-format-version: '17' check-path: ${{ matrix.path }} diff --git a/.github/workflows/ios_ci.yml b/.github/workflows/ios_ci.yml index 0751602c0..fef52a18e 100644 --- a/.github/workflows/ios_ci.yml +++ b/.github/workflows/ios_ci.yml @@ -13,15 +13,15 @@ jobs: ios-compile: runs-on: macos-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Cache cocoapods - uses: actions/cache@v3 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ./examples/GumTestApp/ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} From 287fca200109cda7517c026e0d9519a83f7ca3bc Mon Sep 17 00:00:00 2001 From: davidliu Date: Tue, 26 May 2026 23:26:13 +0900 Subject: [PATCH 07/36] android: bump libwebrtc to 144.7559.05 (#83) --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index be011bf7e..feed005f0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,6 +31,6 @@ android { dependencies { implementation "com.facebook.react:react-android:+" - api 'io.github.webrtc-sdk:android:144.7559.04' + api 'io.github.webrtc-sdk:android:144.7559.05' implementation "androidx.core:core:1.7.0" } From 7713a8383d78df42d4dde30b100bd475e0f56a73 Mon Sep 17 00:00:00 2001 From: davidliu Date: Tue, 26 May 2026 23:36:42 +0900 Subject: [PATCH 08/36] release: 144.1.0 (#86) 287fca2 android: bump libwebrtc to 144.7559.05 (#83) ( davidliu 2026-05-26 23:26:13 +0900) bfb1b73 chore(pinact): pin/update GitHub Actions (#85) ( davidliu 2026-05-26 23:11:16 +0900) 1e99057 ci: pinact (#84) ( davidliu 2026-05-26 20:11:48 +0900) 4880685 release: 144.1.0-beta.2 (#82) ( davidliu 2026-04-21 17:33:02 +0900) 2be039f Bump to lib 144.7559.04 (#81) ( Hiroshi Horie 2026-04-21 16:24:44 +0800) c145d88 release: 144.1.0-beta.1 (#79) ( davidliu 2026-04-06 19:24:57 +0900) d3a6f04 android: add registerTrack methods for 3rd party track registration (#78) ( davidliu 2026-04-06 19:09:30 +0900) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ad22a4dc..89049307e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@livekit/react-native-webrtc", - "version": "144.1.0-beta.2", + "version": "144.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@livekit/react-native-webrtc", - "version": "144.1.0-beta.2", + "version": "144.1.0", "license": "MIT", "dependencies": { "base64-js": "1.5.1", diff --git a/package.json b/package.json index c6c0f4886..775469c96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@livekit/react-native-webrtc", - "version": "144.1.0-beta.2", + "version": "144.1.0", "repository": { "type": "git", "url": "git+https://github.com/livekit/react-native-webrtc.git" From e5d87818289d36411d82d250ab1aee25fcf6c9cd Mon Sep 17 00:00:00 2001 From: Calinteodor Date: Thu, 4 Jun 2026 13:03:43 +0300 Subject: [PATCH 09/36] ios: fix trigger broadcast picker on new arch --- .../ScreenCapturePickerViewManager.m | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/ios/RCTWebRTC/ScreenCapturePickerViewManager.m b/ios/RCTWebRTC/ScreenCapturePickerViewManager.m index d5c2101fe..91e0c4c2c 100644 --- a/ios/RCTWebRTC/ScreenCapturePickerViewManager.m +++ b/ios/RCTWebRTC/ScreenCapturePickerViewManager.m @@ -1,6 +1,6 @@ #if TARGET_OS_IOS -#import +#import #import #import "ScreenCapturePickerViewManager.h" @@ -30,29 +30,28 @@ - (NSString *)preferredExtension { } RCT_EXPORT_METHOD(show : (nonnull NSNumber *)reactTag) { - [self.bridge.uiManager - addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - id view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RPSystemBroadcastPickerView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting " - @"RPSystemBroadcastPickerView, got: %@", - view); - } else { - // Simulate a click - UIButton *btn = nil; - - for (UIView *subview in ((RPSystemBroadcastPickerView *)view).subviews) { - if ([subview isKindOfClass:[UIButton class]]) { - btn = (UIButton *)subview; - } - } - if (btn != nil) { - [btn sendActionsForControlEvents:UIControlEventTouchUpInside]; - } else { - RCTLogError(@"RPSystemBroadcastPickerView button not found"); - } + dispatch_async(dispatch_get_main_queue(), ^{ + RPSystemBroadcastPickerView *picker = self->_broadcastPickerView; + if (![picker isKindOfClass:[RPSystemBroadcastPickerView class]]) { + RCTLogError(@"Invalid broadcast picker view, expecting " + @"RPSystemBroadcastPickerView, got: %@", + picker); + return; + } + + UIButton *btn = nil; + + for (UIView *subview in picker.subviews) { + if ([subview isKindOfClass:[UIButton class]]) { + btn = (UIButton *)subview; } - }]; + } + if (btn != nil) { + [btn sendActionsForControlEvents:UIControlEventTouchUpInside]; + } else { + RCTLogError(@"RPSystemBroadcastPickerView button not found"); + } + }); } @end From 196cbb375755bbeb1c384cf6d114481e82529885 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 18 Jun 2026 03:49:07 +0900 Subject: [PATCH 10/36] fix(ios): bound AudioDeviceModuleObserver JS waits to break the deadlock (#90) ## Problem On iOS the six `RTCAudioDeviceModule` delegate callbacks in `AudioDeviceModuleObserver` block the native audio worker thread on `dispatch_semaphore_wait(..., DISPATCH_TIME_FOREVER)` while waiting for a JS reply. If the JS thread is at the same time parked inside a blocking-synchronous bridge call (for example `peerConnectionAddTransceiver`, which runs `dispatch_sync(workerQueue)` into libwebrtc and back onto the worker thread that is running this delegate), the reply never arrives and the app freezes permanently. There is no crash, every React Native touchable goes dead, and the only recourse is force-killing the app. In practice this is triggered by publishing a microphone track and then a camera track back-to-back right after connect, or by subscribing to a remote audio-plus-video peer on join. The mic publish flips the engine from playout-only to duplex, and the camera publish issues the synchronous `addTransceiver` that lands in the same few-millisecond window. Refs livekit/client-sdk-react-native#389 and #89. ## Fix 1. Bound each of the six waits to 2 seconds instead of waiting forever. On timeout the observer logs through `os_log` and returns the default value of 0 (proceed), so the engine operation degrades gracefully instead of deadlocking. The timeout itself is what breaks the circular wait, because it releases the worker thread. 2. Add a request-id echo so a late reply from a round that already timed out cannot be misattributed to the next round. Native stamps every event with a monotonic id, JS echoes it back on resolve, and the observer only accepts a resolve whose id matches the in-flight round. A small pre-send drain covers the narrow case where a matching reply signals just past its round deadline. Returning 0 on timeout rather than an error code is intentional. A non-zero return makes libwebrtc roll back the engine operation, and the callers in `AudioState` do not retry and ignore the `StartRecording` return value, so an error would leave audio silently broken with no recovery. Returning 0 also matches the existing behavior when no JS handler is registered. ## Scope Fully contained in this package. The request-id stays internal to `react-native-webrtc` and is stripped before the app-facing handler runs, so the public handler API is unchanged and no changes are needed in `@livekit/react-native`. ## Testing - `tsc --noEmit` and `eslint --max-warnings 0` pass. - Not yet built in a host app. Compilation and a real-device repro of the publish race are still to be done. --- ios/RCTWebRTC/AudioDeviceModuleObserver.h | 15 +- ios/RCTWebRTC/AudioDeviceModuleObserver.m | 279 +++++++++++++----- .../WebRTCModule+RTCAudioDeviceModule.m | 36 ++- src/AudioDeviceModuleEvents.ts | 38 ++- 4 files changed, 264 insertions(+), 104 deletions(-) diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.h b/ios/RCTWebRTC/AudioDeviceModuleObserver.h index a1f415909..16f61851d 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.h +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.h @@ -7,13 +7,14 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithWebRTCModule:(WebRTCModule *)module; -// Methods to receive results from JS -- (void)resolveEngineCreatedWithResult:(NSInteger)result; -- (void)resolveWillEnableEngineWithResult:(NSInteger)result; -- (void)resolveWillStartEngineWithResult:(NSInteger)result; -- (void)resolveDidStopEngineWithResult:(NSInteger)result; -- (void)resolveDidDisableEngineWithResult:(NSInteger)result; -- (void)resolveWillReleaseEngineWithResult:(NSInteger)result; +// Methods to receive results from JS. requestId echoes the id sent with the +// corresponding event so stale responses from timed-out rounds can be dropped. +- (void)resolveEngineCreatedWithRequestId:(NSInteger)requestId result:(NSInteger)result; +- (void)resolveWillEnableEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result; +- (void)resolveWillStartEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result; +- (void)resolveDidStopEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result; +- (void)resolveDidDisableEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result; +- (void)resolveWillReleaseEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result; @end diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.m b/ios/RCTWebRTC/AudioDeviceModuleObserver.m index b619a5c37..a12b59772 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.m +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.m @@ -1,8 +1,34 @@ #import "AudioDeviceModuleObserver.h" #import +#import NS_ASSUME_NONNULL_BEGIN +// Upper bound on how long a delegate callback parks the native audio thread +// waiting for JS to respond. The wait used to be DISPATCH_TIME_FOREVER, which +// deadlocks the app if the JS thread is itself blocked inside a synchronous +// bridge call (e.g. peerConnectionAddTransceiver) that is transitively waiting +// on this same audio operation. Bounding the wait turns that permanent freeze +// into a recoverable stall: on timeout we return the default 0 ("proceed") so +// the engine operation degrades gracefully. +// +// Caveat for willEnableEngine: returning 0 here lets the engine proceed even +// though the JS handler (which configures/activates the AVAudioSession) never +// completed. libwebrtc re-validates the session *category* after this callback +// but not that it was *activated*, so a timeout on willEnableEngine can start +// the engine against an unconfigured session. That degraded-but-recoverable +// outcome is deliberately preferred over the unrecoverable deadlock. +static const int64_t kJSResponseTimeoutSeconds = 2; + +static os_log_t ADMObserverLog(void) { + static os_log_t log; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + log = os_log_create("com.livekit.react-native-webrtc", "AudioDeviceModuleObserver"); + }); + return log; +} + @interface AudioDeviceModuleObserver () @property(weak, nonatomic) WebRTCModule *module; @@ -20,6 +46,15 @@ @interface AudioDeviceModuleObserver () @property(nonatomic, assign) NSInteger didDisableEngineResult; @property(nonatomic, assign) NSInteger willReleaseEngineResult; +// Monotonic id stamped on every event sent to JS, and the id of the round +// currently being awaited (0 = none). JS echoes the id back when it resolves; +// the observer only accepts a resolve whose id matches the in-flight round, so a +// late resolve from a round that already timed out cannot be misattributed to +// the next round. Both are guarded by @synchronized(self) since the send side +// runs on the native audio thread and the resolve side on the JS thread. +@property(nonatomic, assign) NSInteger requestIdSeq; +@property(nonatomic, assign) NSInteger awaitingRequestId; + @end @implementation AudioDeviceModuleObserver @@ -38,6 +73,57 @@ - (instancetype)initWithWebRTCModule:(WebRTCModule *)module { return self; } +#pragma mark - Bounded JS round-trip + +// Sends an event to JS and blocks the calling (native audio) thread until JS +// resolves the matching semaphore or kJSResponseTimeoutSeconds elapses. +// +// Each round is tagged with a unique requestId that JS echoes back on resolve; +// -resolveRequestId:... drops any resolve whose id does not match the in-flight +// round, so a late resolve from a previously timed-out round cannot signal this +// round's semaphore. The pre-send drain below is the remaining safety net: it +// clears a stray signal from the narrow case where a matching resolve raced the +// previous round past its timeout deadline (signalling after that round had +// already given up). No legitimate signal for *this* round can exist at drain +// time because the event has not been sent yet. +// +// Returns the JS-provided result on success, or 0 on timeout. +- (NSInteger)sendEventAndWaitWithName:(NSString *)eventName + body:(NSDictionary *)body + semaphore:(dispatch_semaphore_t)semaphore + resultBlock:(NSInteger (^)(void))resultBlock { + NSInteger requestId; + @synchronized(self) { + requestId = ++self.requestIdSeq; + self.awaitingRequestId = requestId; + } + + while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW) == 0) { + // Drain a stray signal left by a resolve that raced the previous round's timeout. + } + + NSMutableDictionary *payload = [body mutableCopy]; + payload[@"requestId"] = @(requestId); + [self.module sendEventWithName:eventName body:payload]; + + dispatch_time_t deadline = dispatch_time(DISPATCH_TIME_NOW, kJSResponseTimeoutSeconds * NSEC_PER_SEC); + if (dispatch_semaphore_wait(semaphore, deadline) != 0) { + @synchronized(self) { + // Stop accepting this round's resolve; if it arrives now it is stale. + if (self.awaitingRequestId == requestId) { + self.awaitingRequestId = 0; + } + } + os_log_error(ADMObserverLog(), + "Timed out after %llds waiting for JS to respond to %{public}@; returning default 0", + (long long)kJSResponseTimeoutSeconds, + eventName); + return 0; + } + + return resultBlock(); +} + #pragma mark - RTCAudioDeviceModuleDelegate - (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -55,13 +141,15 @@ - (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didCreateEngine:(AVAudioEngine *)engine { RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineCreated body:@{}]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.engineCreatedSemaphore, DISPATCH_TIME_FOREVER); + NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineCreated + body:@{} + semaphore:self.engineCreatedSemaphore + resultBlock:^NSInteger { + return self.engineCreatedResult; + }]; - RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)self.engineCreatedResult); - return self.engineCreatedResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)result); + return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -72,21 +160,22 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillEnable - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; + NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillEnable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + } + semaphore:self.willEnableEngineSemaphore + resultBlock:^NSInteger { + return self.willEnableEngineResult; + }]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willEnableEngineSemaphore, DISPATCH_TIME_FOREVER); - - RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)self.willEnableEngineResult); + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)result); AVAudioSession *audioSession = [AVAudioSession sharedInstance]; RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category); - return self.willEnableEngineResult; + return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -97,17 +186,18 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillStart - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willStartEngineSemaphore, DISPATCH_TIME_FOREVER); - - RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)self.willStartEngineResult); - return self.willStartEngineResult; + NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillStart + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + } + semaphore:self.willStartEngineSemaphore + resultBlock:^NSInteger { + return self.willStartEngineResult; + }]; + + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)result); + return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -118,17 +208,18 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidStop - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.didStopEngineSemaphore, DISPATCH_TIME_FOREVER); - - RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)self.didStopEngineResult); - return self.didStopEngineResult; + NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineDidStop + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + } + semaphore:self.didStopEngineSemaphore + resultBlock:^NSInteger { + return self.didStopEngineResult; + }]; + + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)result); + return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -139,29 +230,32 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidDisable - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.didDisableEngineSemaphore, DISPATCH_TIME_FOREVER); - - RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)self.didDisableEngineResult); - return self.didDisableEngineResult; + NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineDidDisable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + } + semaphore:self.didDisableEngineSemaphore + resultBlock:^NSInteger { + return self.didDisableEngineResult; + }]; + + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)result); + return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willReleaseEngine:(AVAudioEngine *)engine { RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillRelease body:@{}]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willReleaseEngineSemaphore, DISPATCH_TIME_FOREVER); + NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillRelease + body:@{} + semaphore:self.willReleaseEngineSemaphore + resultBlock:^NSInteger { + return self.willReleaseEngineResult; + }]; - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)self.willReleaseEngineResult); - return self.willReleaseEngineResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)result); + return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -192,34 +286,73 @@ - (void)audioDeviceModuleDidUpdateDevices:(RTCAudioDeviceModule *)audioDeviceMod #pragma mark - Resolve methods from JS -- (void)resolveEngineCreatedWithResult:(NSInteger)result { - self.engineCreatedResult = result; - dispatch_semaphore_signal(self.engineCreatedSemaphore); +// Applies a JS response only if its requestId matches the round currently being +// awaited. A non-matching id means the round already timed out and moved on, so +// the response is dropped without touching the result or signalling — preventing +// a stale value from being handed to a later round. +- (void)resolveRequestId:(NSInteger)requestId store:(void (^)(void))store semaphore:(dispatch_semaphore_t)semaphore { + @synchronized(self) { + if (requestId != self.awaitingRequestId) { + return; + } + self.awaitingRequestId = 0; + store(); + // Signal inside the lock so this signal is always posted before the next + // round's send-side @synchronized block can start. Otherwise a resolve + // preempted between releasing the lock and signalling could, across a round + // boundary, wake the next round and hand it this round's result. Keeping it + // inside the lock also lets the next round's pre-send drain reliably clear + // any stray signal left by a resolve that raced its own timeout. + dispatch_semaphore_signal(semaphore); + } +} + +- (void)resolveEngineCreatedWithRequestId:(NSInteger)requestId result:(NSInteger)result { + [self resolveRequestId:requestId + store:^{ + self.engineCreatedResult = result; + } + semaphore:self.engineCreatedSemaphore]; } -- (void)resolveWillEnableEngineWithResult:(NSInteger)result { - self.willEnableEngineResult = result; - dispatch_semaphore_signal(self.willEnableEngineSemaphore); +- (void)resolveWillEnableEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result { + [self resolveRequestId:requestId + store:^{ + self.willEnableEngineResult = result; + } + semaphore:self.willEnableEngineSemaphore]; } -- (void)resolveWillStartEngineWithResult:(NSInteger)result { - self.willStartEngineResult = result; - dispatch_semaphore_signal(self.willStartEngineSemaphore); +- (void)resolveWillStartEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result { + [self resolveRequestId:requestId + store:^{ + self.willStartEngineResult = result; + } + semaphore:self.willStartEngineSemaphore]; } -- (void)resolveDidStopEngineWithResult:(NSInteger)result { - self.didStopEngineResult = result; - dispatch_semaphore_signal(self.didStopEngineSemaphore); +- (void)resolveDidStopEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result { + [self resolveRequestId:requestId + store:^{ + self.didStopEngineResult = result; + } + semaphore:self.didStopEngineSemaphore]; } -- (void)resolveDidDisableEngineWithResult:(NSInteger)result { - self.didDisableEngineResult = result; - dispatch_semaphore_signal(self.didDisableEngineSemaphore); +- (void)resolveDidDisableEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result { + [self resolveRequestId:requestId + store:^{ + self.didDisableEngineResult = result; + } + semaphore:self.didDisableEngineSemaphore]; } -- (void)resolveWillReleaseEngineWithResult:(NSInteger)result { - self.willReleaseEngineResult = result; - dispatch_semaphore_signal(self.willReleaseEngineSemaphore); +- (void)resolveWillReleaseEngineWithRequestId:(NSInteger)requestId result:(NSInteger)result { + [self resolveRequestId:requestId + store:^{ + self.willReleaseEngineResult = result; + } + semaphore:self.willReleaseEngineSemaphore]; } @end diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index fad0f7c3f..0aa296bb9 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -229,33 +229,45 @@ @implementation WebRTCModule (RTCAudioDeviceModule) #pragma mark - Observer Delegate Response Methods -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveEngineCreated : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveEngineCreatedWithResult:result]; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveEngineCreated + : (NSInteger)requestId result + : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveEngineCreatedWithRequestId:requestId result:result]; return nil; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillEnableEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillEnableEngineWithResult:result]; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillEnableEngine + : (NSInteger)requestId result + : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveWillEnableEngineWithRequestId:requestId result:result]; return nil; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillStartEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillStartEngineWithResult:result]; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillStartEngine + : (NSInteger)requestId result + : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveWillStartEngineWithRequestId:requestId result:result]; return nil; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidStopEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveDidStopEngineWithResult:result]; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidStopEngine + : (NSInteger)requestId result + : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveDidStopEngineWithRequestId:requestId result:result]; return nil; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidDisableEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveDidDisableEngineWithResult:result]; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidDisableEngine + : (NSInteger)requestId result + : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveDidDisableEngineWithRequestId:requestId result:result]; return nil; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillReleaseEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillReleaseEngineWithResult:result]; +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillReleaseEngine + : (NSInteger)requestId result + : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveWillReleaseEngineWithRequestId:requestId result:result]; return nil; } diff --git a/src/AudioDeviceModuleEvents.ts b/src/AudioDeviceModuleEvents.ts index 5c63c212e..f830a1a46 100644 --- a/src/AudioDeviceModuleEvents.ts +++ b/src/AudioDeviceModuleEvents.ts @@ -15,6 +15,18 @@ export interface EngineStateEventData { isRecordingEnabled: boolean; } +/** + * Raw native event payload. Every engine event carries a `requestId` that must + * be echoed back to the matching resolve call so the native side can drop a + * response from a round that already timed out. This id is internal and is not + * surfaced to app-registered handlers. + */ +interface EngineEventPayload { + requestId: number; +} + +interface EngineStateEventPayload extends EngineEventPayload, EngineStateEventData {} + export type AudioDeviceModuleEventType = | 'speechActivity' | 'devicesUpdated'; @@ -50,7 +62,8 @@ class AudioDeviceModuleEventEmitter { public setupListeners() { if (Platform.OS !== 'android' && WebRTCModule) { // Setup handlers for blocking delegate methods - addListener(this, 'audioDeviceModuleEngineCreated', async () => { + addListener(this, 'audioDeviceModuleEngineCreated', async (event: unknown) => { + const { requestId } = event as EngineEventPayload; let result = 0; if (this.engineCreatedHandler) { @@ -62,14 +75,14 @@ class AudioDeviceModuleEventEmitter { } } - WebRTCModule.audioDeviceModuleResolveEngineCreated(result); + WebRTCModule.audioDeviceModuleResolveEngineCreated(requestId, result); }); addListener( this, 'audioDeviceModuleEngineWillEnable', async (event: unknown) => { - const { isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventData; + const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.willEnableEngineHandler) { @@ -81,7 +94,7 @@ class AudioDeviceModuleEventEmitter { } } - WebRTCModule.audioDeviceModuleResolveWillEnableEngine(result); + WebRTCModule.audioDeviceModuleResolveWillEnableEngine(requestId, result); }, ); @@ -89,7 +102,7 @@ class AudioDeviceModuleEventEmitter { this, 'audioDeviceModuleEngineWillStart', async (event: unknown) => { - const { isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventData; + const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.willStartEngineHandler) { @@ -101,7 +114,7 @@ class AudioDeviceModuleEventEmitter { } } - WebRTCModule.audioDeviceModuleResolveWillStartEngine(result); + WebRTCModule.audioDeviceModuleResolveWillStartEngine(requestId, result); }, ); @@ -109,7 +122,7 @@ class AudioDeviceModuleEventEmitter { this, 'audioDeviceModuleEngineDidStop', async (event: unknown) => { - const { isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventData; + const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.didStopEngineHandler) { @@ -121,7 +134,7 @@ class AudioDeviceModuleEventEmitter { } } - WebRTCModule.audioDeviceModuleResolveDidStopEngine(result); + WebRTCModule.audioDeviceModuleResolveDidStopEngine(requestId, result); }, ); @@ -129,7 +142,7 @@ class AudioDeviceModuleEventEmitter { this, 'audioDeviceModuleEngineDidDisable', async (event: unknown) => { - const { isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventData; + const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.didDisableEngineHandler) { @@ -141,11 +154,12 @@ class AudioDeviceModuleEventEmitter { } } - WebRTCModule.audioDeviceModuleResolveDidDisableEngine(result); + WebRTCModule.audioDeviceModuleResolveDidDisableEngine(requestId, result); }, ); - addListener(this, 'audioDeviceModuleEngineWillRelease', async () => { + addListener(this, 'audioDeviceModuleEngineWillRelease', async (event: unknown) => { + const { requestId } = event as EngineEventPayload; let result = 0; if (this.willReleaseEngineHandler) { @@ -157,7 +171,7 @@ class AudioDeviceModuleEventEmitter { } } - WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(result); + WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(requestId, result); }); } } From c751bd67ddd2a137c9be9be712eba899a6c08c74 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:11:31 +0900 Subject: [PATCH 11/36] ios: bump WebRTC-SDK to 144.7559.10 (#92) Adapt the voice-processing setter/getter to the 144.7559.10 API. --- ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m | 4 ++-- livekit-react-native-webrtc.podspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index 0aa296bb9..58f1a015d 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -102,7 +102,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (BOOL)enabled resolver : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; + NSInteger result = [self.audioDeviceModule setPlatformVoiceProcessingAllowed:enabled]; if (result == 0) { resolve(nil); } else { @@ -113,7 +113,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingEnabled) { - return @(self.audioDeviceModule.isVoiceProcessingEnabled); + return @(self.audioDeviceModule.platformAudioProcessingState.isVoiceProcessingEnabledRequested); } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingBypassed : (BOOL)bypassed) { diff --git a/livekit-react-native-webrtc.podspec b/livekit-react-native-webrtc.podspec index 6fbcbc450..cf4e1059a 100644 --- a/livekit-react-native-webrtc.podspec +++ b/livekit-react-native-webrtc.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.libraries = 'c', 'sqlite3', 'stdc++' s.framework = 'AudioToolbox','AVFoundation', 'CoreAudio', 'CoreGraphics', 'CoreVideo', 'GLKit', 'VideoToolbox' s.dependency 'React-Core' - s.dependency 'WebRTC-SDK', '=144.7559.04' + s.dependency 'WebRTC-SDK', '=144.7559.10' # Swift/Objective-C compatibility s.pod_target_xcconfig = { From a95d57d3731071333cc8dc4f06bea4cfa8a967e1 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:20:29 +0900 Subject: [PATCH 12/36] fix(ios): skip AudioDeviceModule JS round-trips when no handler is registered (#91) ## Summary Builds on #90 (the bounded-wait deadlock fix) to remove the JS round-trip entirely for AudioDeviceModule engine hooks that have no registered handler, instead of only bounding the wait. For those hooks this closes the deadlock window rather than capping it at 2 seconds. Stacked on #90. This PR targets the #90 branch, so it should land after #90 and the diff here is only the delta on top. ## Background #90 bounds each of the six RTCAudioDeviceModule delegate waits to 2 seconds so a stuck JS thread can no longer freeze the app forever. But a hook with no JS handler has nothing to wait for, so blocking it at all is pure risk with no benefit. ## Change Track per-hook handler registration with is-prefixed BOOL active flags. The JS layer pushes a flag to native whenever a handler is set or cleared. When a hook is inactive the observer returns 0 immediately without sending the event or waiting, so the unhandled hooks never enter the blocking path and cannot contribute to the deadlock. In stock config this removes engineCreated, willStart, didStop and willRelease from the blocking path entirely, including willStart, which appeared in both reported freeze traces. The two hooks LiveKit registers by default (willEnable and didDisable) keep the bounded-wait and request-id safety net from #90. The flags are written on the JS thread (handler registration) and read on the native audio thread (delegate callbacks), so they are declared atomic. The multi-field request-id state stays under @synchronized because it needs a true critical section. ## Scope Self-contained in this package. The active flags and request ids are internal to react-native-webrtc and never reach app handlers, so the public handler API is unchanged and no changes are needed in @livekit/react-native. ## Testing - npm run lint (eslint and tsc) passes. - clang-format check passes. - iOS and Android native builds run in CI. Refs #89, livekit/client-sdk-react-native#389. --- ios/RCTWebRTC/AudioDeviceModuleObserver.h | 11 ++ ios/RCTWebRTC/AudioDeviceModuleObserver.m | 124 ++++++++++++----- .../WebRTCModule+RTCAudioDeviceModule.m | 32 +++++ src/AudioDeviceModuleEvents.ts | 125 +++++++++++++++++- 4 files changed, 256 insertions(+), 36 deletions(-) diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.h b/ios/RCTWebRTC/AudioDeviceModuleObserver.h index 16f61851d..1a3702549 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.h +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.h @@ -7,6 +7,17 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithWebRTCModule:(WebRTCModule *)module; +// Tracks whether each JS handler is registered. When NO, the observer returns +// immediately without a JS round trip, avoiding the deadlock window entirely. +// Atomic because they are written on the JS thread (handler registration) and +// read on the native audio thread (delegate callbacks). +@property(atomic, assign) BOOL isEngineCreatedActive; +@property(atomic, assign) BOOL isWillEnableEngineActive; +@property(atomic, assign) BOOL isWillStartEngineActive; +@property(atomic, assign) BOOL isDidStopEngineActive; +@property(atomic, assign) BOOL isDidDisableEngineActive; +@property(atomic, assign) BOOL isWillReleaseEngineActive; + // Methods to receive results from JS. requestId echoes the id sent with the // corresponding event so stale responses from timed-out rounds can be dropped. - (void)resolveEngineCreatedWithRequestId:(NSInteger)requestId result:(NSInteger)result; diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.m b/ios/RCTWebRTC/AudioDeviceModuleObserver.m index a12b59772..8b42cf11a 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.m +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.m @@ -69,6 +69,17 @@ - (instancetype)initWithWebRTCModule:(WebRTCModule *)module { _didStopEngineSemaphore = dispatch_semaphore_create(0); _didDisableEngineSemaphore = dispatch_semaphore_create(0); _willReleaseEngineSemaphore = dispatch_semaphore_create(0); + + // Default every hook to active so a delegate callback that fires before JS + // has reconciled its handler state (e.g. a recreated observer) still does the + // bounded round trip rather than silently skipping a handler's veto. JS + // reconciles these to the real handler state in setupListeners(). + _isEngineCreatedActive = YES; + _isWillEnableEngineActive = YES; + _isWillStartEngineActive = YES; + _isDidStopEngineActive = YES; + _isDidDisableEngineActive = YES; + _isWillReleaseEngineActive = YES; } return self; } @@ -87,11 +98,22 @@ - (instancetype)initWithWebRTCModule:(WebRTCModule *)module { // already given up). No legitimate signal for *this* round can exist at drain // time because the event has not been sent yet. // -// Returns the JS-provided result on success, or 0 on timeout. +// If isActive is NO, returns 0 immediately without a JS round trip, avoiding the +// deadlock window when no handler is registered. +// +// Returns the JS-provided result on success, or 0 on timeout or when inactive. - (NSInteger)sendEventAndWaitWithName:(NSString *)eventName body:(NSDictionary *)body semaphore:(dispatch_semaphore_t)semaphore - resultBlock:(NSInteger (^)(void))resultBlock { + resultBlock:(NSInteger (^)(void))resultBlock + isActive:(BOOL)isActive { + if (!isActive) { + // No handler registered, proceed immediately without JS round trip. + // This avoids the deadlock window entirely. + os_log_debug(ADMObserverLog(), "Skipping JS round-trip for %{public}@ (no handler registered)", eventName); + return 0; + } + NSInteger requestId; @synchronized(self) { requestId = ++self.requestIdSeq; @@ -139,16 +161,23 @@ - (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didCreateEngine:(AVAudioEngine *)engine { - RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); + BOOL isActive = self.isEngineCreatedActive; + + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); + } NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineCreated body:@{} semaphore:self.engineCreatedSemaphore resultBlock:^NSInteger { return self.engineCreatedResult; - }]; + } + isActive:isActive]; - RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)result); + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)result); + } return result; } @@ -156,9 +185,13 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willEnableEngine:(AVAudioEngine *)engine isPlayoutEnabled:(BOOL)isPlayoutEnabled isRecordingEnabled:(BOOL)isRecordingEnabled { - RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - playout: %d, recording: %d - waiting for JS response", - isPlayoutEnabled, - isRecordingEnabled); + BOOL isActive = self.isWillEnableEngineActive; + + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); + } NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillEnable body:@{ @@ -168,12 +201,15 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule semaphore:self.willEnableEngineSemaphore resultBlock:^NSInteger { return self.willEnableEngineResult; - }]; + } + isActive:isActive]; - RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)result); + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)result); - AVAudioSession *audioSession = [AVAudioSession sharedInstance]; - RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category); + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category); + } return result; } @@ -182,9 +218,13 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willStartEngine:(AVAudioEngine *)engine isPlayoutEnabled:(BOOL)isPlayoutEnabled isRecordingEnabled:(BOOL)isRecordingEnabled { - RCTLog(@"[AudioDeviceModuleObserver] Engine will start - playout: %d, recording: %d - waiting for JS response", - isPlayoutEnabled, - isRecordingEnabled); + BOOL isActive = self.isWillStartEngineActive; + + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); + } NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillStart body:@{ @@ -194,9 +234,12 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule semaphore:self.willStartEngineSemaphore resultBlock:^NSInteger { return self.willStartEngineResult; - }]; + } + isActive:isActive]; - RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)result); + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)result); + } return result; } @@ -204,9 +247,13 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didStopEngine:(AVAudioEngine *)engine isPlayoutEnabled:(BOOL)isPlayoutEnabled isRecordingEnabled:(BOOL)isRecordingEnabled { - RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - playout: %d, recording: %d - waiting for JS response", - isPlayoutEnabled, - isRecordingEnabled); + BOOL isActive = self.isDidStopEngineActive; + + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); + } NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineDidStop body:@{ @@ -216,9 +263,12 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule semaphore:self.didStopEngineSemaphore resultBlock:^NSInteger { return self.didStopEngineResult; - }]; + } + isActive:isActive]; - RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)result); + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)result); + } return result; } @@ -226,9 +276,13 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didDisableEngine:(AVAudioEngine *)engine isPlayoutEnabled:(BOOL)isPlayoutEnabled isRecordingEnabled:(BOOL)isRecordingEnabled { - RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - playout: %d, recording: %d - waiting for JS response", - isPlayoutEnabled, - isRecordingEnabled); + BOOL isActive = self.isDidDisableEngineActive; + + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); + } NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineDidDisable body:@{ @@ -238,23 +292,33 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule semaphore:self.didDisableEngineSemaphore resultBlock:^NSInteger { return self.didDisableEngineResult; - }]; + } + isActive:isActive]; - RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)result); + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)result); + } return result; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willReleaseEngine:(AVAudioEngine *)engine { - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); + BOOL isActive = self.isWillReleaseEngineActive; + + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); + } NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillRelease body:@{} semaphore:self.willReleaseEngineSemaphore resultBlock:^NSInteger { return self.willReleaseEngineResult; - }]; + } + isActive:isActive]; - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)result); + if (isActive) { + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)result); + } return result; } diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index 58f1a015d..0eb3d0aab 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -271,4 +271,36 @@ @implementation WebRTCModule (RTCAudioDeviceModule) return nil; } +#pragma mark - Handler Active State + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetEngineCreatedActive : (BOOL)isActive) { + self.audioDeviceModuleObserver.isEngineCreatedActive = isActive; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetWillEnableEngineActive : (BOOL)isActive) { + self.audioDeviceModuleObserver.isWillEnableEngineActive = isActive; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetWillStartEngineActive : (BOOL)isActive) { + self.audioDeviceModuleObserver.isWillStartEngineActive = isActive; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDidStopEngineActive : (BOOL)isActive) { + self.audioDeviceModuleObserver.isDidStopEngineActive = isActive; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDidDisableEngineActive : (BOOL)isActive) { + self.audioDeviceModuleObserver.isDidDisableEngineActive = isActive; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetWillReleaseEngineActive : (BOOL)isActive) { + self.audioDeviceModuleObserver.isWillReleaseEngineActive = isActive; + return nil; +} + @end diff --git a/src/AudioDeviceModuleEvents.ts b/src/AudioDeviceModuleEvents.ts index f830a1a46..a93365574 100644 --- a/src/AudioDeviceModuleEvents.ts +++ b/src/AudioDeviceModuleEvents.ts @@ -59,8 +59,22 @@ class AudioDeviceModuleEventEmitter { private didDisableEngineHandler: AudioEngineEventHandler | null = null; private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; + private listenersSetUp = false; + public setupListeners() { if (Platform.OS !== 'android' && WebRTCModule) { + // Reconcile on every call so a recreated native observer (which defaults + // every hook to active) is re-synced to the current handler state even + // when JS listeners are already registered. addListener appends without + // de-duping, so the registration below must run only once. + if (this.listenersSetUp) { + this.reconcileActiveFlags(); + + return; + } + + this.listenersSetUp = true; + // Setup handlers for blocking delegate methods addListener(this, 'audioDeviceModuleEngineCreated', async (event: unknown) => { const { requestId } = event as EngineEventPayload; @@ -173,6 +187,8 @@ class AudioDeviceModuleEventEmitter { WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(requestId, result); }); + + this.reconcileActiveFlags(); } } @@ -204,12 +220,74 @@ class AudioDeviceModuleEventEmitter { removeListener(listener); } + /** + * Push a handler's active state to native. The native active-flag setters are + * iOS/macOS only, so this is gated like setupListeners() to avoid a TypeError + * on Android, where these methods do not exist. + */ + private pushHandlerActive(method: string, isActive: boolean) { + if (Platform.OS !== 'android' && WebRTCModule) { + WebRTCModule[method](isActive); + } + } + + /** + * Apply a handler change while keeping the native active flag ordered so a + * delegate callback racing registration can never skip a handler that exists. + * + * On activation (null to non-null) the flag is pushed active *before* the + * handler is published, so the worst case is a JS round trip whose listener + * sees the not-yet-published handler and resolves 0, never a skipped veto. On + * deactivation the handler is cleared first, then the flag pushed inactive, so + * the same safe ordering holds in reverse. No push happens when active state + * is unchanged. + */ + private applyHandlerActive(method: string, wasActive: boolean, isActive: boolean, assign: () => void) { + if (isActive && !wasActive) { + this.pushHandlerActive(method, true); + } + + assign(); + + if (!isActive && wasActive) { + this.pushHandlerActive(method, false); + } + } + + /** + * Push the current handler state for every hook to native. Native defaults + * each hook to active, so this re-syncs a fresh or recreated observer to the + * handlers registered now rather than relying on a set/clear transition that + * may have already happened. + */ + private reconcileActiveFlags() { + const activeFlags: [string, boolean][] = [ + [ 'audioDeviceModuleSetEngineCreatedActive', this.engineCreatedHandler !== null ], + [ 'audioDeviceModuleSetWillEnableEngineActive', this.willEnableEngineHandler !== null ], + [ 'audioDeviceModuleSetWillStartEngineActive', this.willStartEngineHandler !== null ], + [ 'audioDeviceModuleSetDidStopEngineActive', this.didStopEngineHandler !== null ], + [ 'audioDeviceModuleSetDidDisableEngineActive', this.didDisableEngineHandler !== null ], + [ 'audioDeviceModuleSetWillReleaseEngineActive', this.willReleaseEngineHandler !== null ], + ]; + + for (const [ method, isActive ] of activeFlags) { + this.pushHandlerActive(method, isActive); + } + } + /** * Set handler for engine created delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { - this.engineCreatedHandler = handler; + this.applyHandlerActive( + 'audioDeviceModuleSetEngineCreatedActive', + this.engineCreatedHandler !== null, + handler !== null, + () => { + this.engineCreatedHandler = handler; + }, + ); } /** @@ -217,7 +295,14 @@ class AudioDeviceModuleEventEmitter { * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { - this.willEnableEngineHandler = handler; + this.applyHandlerActive( + 'audioDeviceModuleSetWillEnableEngineActive', + this.willEnableEngineHandler !== null, + handler !== null, + () => { + this.willEnableEngineHandler = handler; + }, + ); } /** @@ -225,7 +310,14 @@ class AudioDeviceModuleEventEmitter { * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { - this.willStartEngineHandler = handler; + this.applyHandlerActive( + 'audioDeviceModuleSetWillStartEngineActive', + this.willStartEngineHandler !== null, + handler !== null, + () => { + this.willStartEngineHandler = handler; + }, + ); } /** @@ -233,7 +325,14 @@ class AudioDeviceModuleEventEmitter { * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { - this.didStopEngineHandler = handler; + this.applyHandlerActive( + 'audioDeviceModuleSetDidStopEngineActive', + this.didStopEngineHandler !== null, + handler !== null, + () => { + this.didStopEngineHandler = handler; + }, + ); } /** @@ -241,7 +340,14 @@ class AudioDeviceModuleEventEmitter { * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { - this.didDisableEngineHandler = handler; + this.applyHandlerActive( + 'audioDeviceModuleSetDidDisableEngineActive', + this.didDisableEngineHandler !== null, + handler !== null, + () => { + this.didDisableEngineHandler = handler; + }, + ); } /** @@ -249,7 +355,14 @@ class AudioDeviceModuleEventEmitter { * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { - this.willReleaseEngineHandler = handler; + this.applyHandlerActive( + 'audioDeviceModuleSetWillReleaseEngineActive', + this.willReleaseEngineHandler !== null, + handler !== null, + () => { + this.willReleaseEngineHandler = handler; + }, + ); } } From d3150934deabcb6105e2f29842b5641bcccf50bd Mon Sep 17 00:00:00 2001 From: davidliu Date: Fri, 19 Jun 2026 01:42:17 +0900 Subject: [PATCH 13/36] release: 144.1.1 (#93) a95d57d fix(ios): skip AudioDeviceModule JS round-trips when no handler is registered (#91) ( Hiroshi Horie 2026-06-18 16:20:29 +0900) c751bd6 ios: bump WebRTC-SDK to 144.7559.10 (#92) ( Hiroshi Horie 2026-06-18 16:11:31 +0900) 196cbb3 fix(ios): bound AudioDeviceModuleObserver JS waits to break the deadlock (#90) ( Hiroshi Horie 2026-06-18 03:49:07 +0900) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 89049307e..ed101483e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@livekit/react-native-webrtc", - "version": "144.1.0", + "version": "144.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@livekit/react-native-webrtc", - "version": "144.1.0", + "version": "144.1.1", "license": "MIT", "dependencies": { "base64-js": "1.5.1", diff --git a/package.json b/package.json index 775469c96..5235e6a3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@livekit/react-native-webrtc", - "version": "144.1.0", + "version": "144.1.1", "repository": { "type": "git", "url": "git+https://github.com/livekit/react-native-webrtc.git" From c4ea2d37e0f5fc4274b47950ae465c346d9dc726 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:46:34 +0800 Subject: [PATCH 14/36] build(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 (#1818) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 524b7d7bf..337752243 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4213,10 +4213,20 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "dependencies": { "argparse": "^2.0.1" }, @@ -8770,9 +8780,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, "requires": { "argparse": "^2.0.1" From 0c6362225a53153c4cc1861416f270c3b287a514 Mon Sep 17 00:00:00 2001 From: Philipp Hancke Date: Sat, 4 Jul 2026 11:25:23 +0200 Subject: [PATCH 15/36] registerGlobals: don't set RTCRtpSender/RTCRtpReceiver twice in particular when the first instance is wrongly setting the sender to the receiver --- src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 230925d6e..e08cfb640 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,8 +83,6 @@ function registerGlobals(): void { global.RTCIceCandidate = RTCIceCandidate; global.RTCCertificate = RTCCertificate; global.RTCPeerConnection = RTCPeerConnection; - global.RTCRtpReceiver = RTCRtpReceiver; - global.RTCRtpSender = RTCRtpReceiver; global.RTCSessionDescription = RTCSessionDescription; global.MediaStream = MediaStream; global.MediaStreamTrack = MediaStreamTrack; From e8face328e3d00ac9cb4cc6e6e9fe64f069929fc Mon Sep 17 00:00:00 2001 From: Philipp Hancke Date: Sun, 5 Jul 2026 07:53:11 +0200 Subject: [PATCH 16/36] dispose the peer connection after signalingState changes to "closed" (#1821) When close() is called, libwebrtc emits the observer state changes in the order iceConnectionState, connectionState, then signalingState (all "closed"). react-native-webrtc was tearing down its listeners on connectionState === "closed", which dropped the subsequent signalingState "closed" event, leaving signalingState stuck at its previous value. Move the teardown to the signalingState === "closed" handler so every final state change is applied before the listeners are removed. See PeerConnection::Close(): https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/pc/peer_connection.cc;l=1818;drc=4e0c079a2b24c7ec577949494e94ba6f5bf264e4 --- src/RTCPeerConnection.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/RTCPeerConnection.ts b/src/RTCPeerConnection.ts index 0c2f98d0a..a0a558654 100644 --- a/src/RTCPeerConnection.ts +++ b/src/RTCPeerConnection.ts @@ -719,13 +719,6 @@ export default class RTCPeerConnection extends EventTarget { @@ -736,6 +729,13 @@ export default class RTCPeerConnection extends EventTarget Date: Sun, 5 Jul 2026 13:56:36 +0800 Subject: [PATCH 17/36] build(deps-dev): bump @babel/core in /examples/GumTestApp_macOS (#1817) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.12.8 to 7.29.6. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.6/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.29.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/GumTestApp_macOS/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/GumTestApp_macOS/package.json b/examples/GumTestApp_macOS/package.json index 9cd663d17..514277e7f 100644 --- a/examples/GumTestApp_macOS/package.json +++ b/examples/GumTestApp_macOS/package.json @@ -18,7 +18,7 @@ "react-native-webrtc": "*" }, "devDependencies": { - "@babel/core": "7.12.8", + "@babel/core": "7.29.6", "@babel/runtime": "7.12.5", "@react-native-community/eslint-config": "0.0.5", "babel-jest": "24.9.0", From 5892a0f54d379605adaf8b44a3c541d45b998ae0 Mon Sep 17 00:00:00 2001 From: Calin-Teodor Date: Wed, 8 Jul 2026 11:27:56 +0300 Subject: [PATCH 18/36] ios(RCTWebRTC): enable Center Stage for devices that support it --- ios/RCTWebRTC/VideoCaptureController.m | 41 +++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index e7f52a10b..0450ebc66 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -51,6 +51,23 @@ - (void)startCapture { return; } + // Enable Center Stage when the device supports it; cooperative with Control Center. + if (@available(iOS 16.0, *)) { + BOOL centerStageSupported = NO; + for (AVCaptureDeviceFormat *fmt in [RTCCameraVideoCapturer supportedFormatsForDevice:self.device]) { + if (fmt.isCenterStageSupported) { + centerStageSupported = YES; + break; + } + } + if (centerStageSupported) { + AVCaptureDevice.centerStageControlMode = AVCaptureCenterStageControlModeCooperative; + AVCaptureDevice.centerStageEnabled = YES; + } else if (AVCaptureDevice.isCenterStageEnabled) { + AVCaptureDevice.centerStageEnabled = NO; + } + } + AVCaptureDeviceFormat *format = [self selectFormatForDevice:self.device withTargetWidth:self.width withTargetHeight:self.height]; @@ -62,6 +79,17 @@ - (void)startCapture { self.selectedFormat = format; + // Clamp fps to the range Center Stage allows for this format. + int fps = self.frameRate; + if (@available(iOS 16.0, *)) { + if (AVCaptureDevice.isCenterStageEnabled) { + AVFrameRateRange *csRange = format.videoFrameRateRangeForCenterStage; + if (csRange) { + fps = MAX((int)csRange.minFrameRate, MIN(fps, (int)csRange.maxFrameRate)); + } + } + } + AVCaptureSession *session = self.capturer.captureSession; if (@available(iOS 16.0, *)) { BOOL enable = self.enableMultitaskingCameraAccess; @@ -83,7 +111,7 @@ - (void)startCapture { __weak VideoCaptureController *weakSelf = self; [self.capturer startCaptureWithDevice:self.device format:format - fps:self.frameRate + fps:fps completionHandler:^(NSError *err) { if (err) { RCTLogError(@"[VideoCaptureController] Error starting capture: %@", err); @@ -270,7 +298,18 @@ - (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device AVCaptureDeviceFormat *selectedFormat = nil; int currentDiff = INT_MAX; + BOOL centerStageEnabled = NO; + if (@available(iOS 16.0, *)) { + centerStageEnabled = AVCaptureDevice.isCenterStageEnabled; + } + for (AVCaptureDeviceFormat *format in formats) { + // Center Stage only permits supported formats. + if (@available(iOS 16.0, *)) { + if (centerStageEnabled && !format.isCenterStageSupported) { + continue; + } + } CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription); FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription); int diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height); From 0a89cc0752f7eaee661661a504ac7ef791a21de6 Mon Sep 17 00:00:00 2001 From: Calin-Teodor Date: Wed, 8 Jul 2026 11:51:04 +0300 Subject: [PATCH 19/36] ci(ios): pin runner to macos-15 (Xcode 16.4) --- .github/workflows/ios_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios_ci.yml b/.github/workflows/ios_ci.yml index 0751602c0..1bb50a255 100644 --- a/.github/workflows/ios_ci.yml +++ b/.github/workflows/ios_ci.yml @@ -11,7 +11,7 @@ on: jobs: ios-compile: - runs-on: macos-latest + runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 78904d2784d6f2f83c791454a994d69a01e3ed2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Fri, 20 Mar 2026 10:08:16 +0100 Subject: [PATCH 20/36] android: fix race condition in getDisplayMedia Fixes: https://github.com/react-native-webrtc/react-native-webrtc/issues/1799 --- .../AbstractVideoCaptureController.java | 8 +++--- .../oney/WebRTCModule/GetUserMediaImpl.java | 18 ++++++++++--- .../WebRTCModule/MediaProjectionService.java | 27 ++++++++++++++++--- .../com/oney/WebRTCModule/ThreadUtils.java | 8 ++++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/AbstractVideoCaptureController.java b/android/src/main/java/com/oney/WebRTCModule/AbstractVideoCaptureController.java index fe1f2b5eb..90955378b 100644 --- a/android/src/main/java/com/oney/WebRTCModule/AbstractVideoCaptureController.java +++ b/android/src/main/java/com/oney/WebRTCModule/AbstractVideoCaptureController.java @@ -1,5 +1,7 @@ package com.oney.WebRTCModule; +import android.util.Log; + import androidx.annotation.Nullable; import androidx.core.util.Consumer; @@ -10,6 +12,8 @@ import org.webrtc.VideoCapturer; public abstract class AbstractVideoCaptureController { + private static final String TAG = AbstractVideoCaptureController.class.getSimpleName(); + protected int targetWidth; protected int targetHeight; protected int targetFps; @@ -78,9 +82,7 @@ public void startCapture() { try { videoCapturer.startCapture(targetWidth, targetHeight, targetFps); } catch (RuntimeException e) { - // XXX This can only fail if we initialize the capturer incorrectly, - // which we don't. Thus, ignore any failures here since we trust - // ourselves. + Log.e(TAG, "Failed to start capture", e); } } diff --git a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java index 2eb79759b..e15cdb88a 100644 --- a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java +++ b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -82,10 +83,19 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, mediaProjectionPermissionResultData = data; - ThreadUtils.runOnExecutor(() -> { - MediaProjectionService.launch(activity); - createScreenStream(); - }); + MediaProjectionService.launch(activity) + .orTimeout(10, TimeUnit.SECONDS) + .whenCompleteAsync((value, error) -> { + if (error != null) { + Log.e(TAG, "Failed to start MediaProjection service", error); + displayMediaPromise.reject("DOMException", "AbortError"); + displayMediaPromise = null; + mediaProjectionPermissionResultData = null; + return; + } + + createScreenStream(); + }, ThreadUtils.getExecutor()); } } }); diff --git a/android/src/main/java/com/oney/WebRTCModule/MediaProjectionService.java b/android/src/main/java/com/oney/WebRTCModule/MediaProjectionService.java index 26e188c45..60e547b5e 100644 --- a/android/src/main/java/com/oney/WebRTCModule/MediaProjectionService.java +++ b/android/src/main/java/com/oney/WebRTCModule/MediaProjectionService.java @@ -11,6 +11,7 @@ import android.util.Log; import java.util.Random; +import java.util.concurrent.CompletableFuture; /** * This class implements an Android {@link Service}, a foreground one specifically, and it's @@ -24,11 +25,17 @@ public class MediaProjectionService extends Service { static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000; - public static void launch(Context context) { + private static volatile CompletableFuture startFuture; + + public static CompletableFuture launch(Context context) { if (!WebRTCModuleOptions.getInstance().enableMediaProjectionService) { - return; + return CompletableFuture.completedFuture(null); } + CompletableFuture future = new CompletableFuture<>(); + + startFuture = future; + MediaProjectionNotification.createNotificationChannel(context); Intent intent = new Intent(context, MediaProjectionService.class); ComponentName componentName; @@ -43,14 +50,21 @@ public static void launch(Context context) { // Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31). // See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions Log.w(TAG, "Media projection service not started", e); - return; + startFuture = null; + future.completeExceptionally(e); + + return future; } if (componentName == null) { Log.w(TAG, "Media projection service not started"); + startFuture = null; + future.completeExceptionally(new RuntimeException("Media projection service not started")); } else { Log.i(TAG, "Media projection service started"); } + + return future; } public static void abort(Context context) { @@ -77,6 +91,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { startForeground(NOTIFICATION_ID, notification); } + CompletableFuture fut = startFuture; + + if (fut != null) { + startFuture = null; + fut.complete(null); + } + return START_NOT_STICKY; } } diff --git a/android/src/main/java/com/oney/WebRTCModule/ThreadUtils.java b/android/src/main/java/com/oney/WebRTCModule/ThreadUtils.java index e735d953b..36106aa5d 100644 --- a/android/src/main/java/com/oney/WebRTCModule/ThreadUtils.java +++ b/android/src/main/java/com/oney/WebRTCModule/ThreadUtils.java @@ -13,6 +13,14 @@ final class ThreadUtils { */ private static final ExecutorService executor = Executors.newSingleThreadExecutor(); + /** + * Gets the executor used to run all WebRTC PeerConnection APIs. + * @return ExecutorService. + */ + public static ExecutorService getExecutor() { + return executor; + } + /** * Runs the given {@link Runnable} on the executor. * @param runnable From b9b5ded6c5c0282f5e2d4107b9b740a6c8ca09d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Tue, 21 Jul 2026 16:13:03 +0200 Subject: [PATCH 21/36] release: 124.0.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 78904d2 android: fix race condition in getDisplayMedia ( Saúl Ibarra Corretgé 2026-03-20 10:08:16 +0100) 0a89cc0 ci(ios): pin runner to macos-15 (Xcode 16.4) ( Calin-Teodor 2026-07-08 11:51:04 +0300) 5892a0f ios(RCTWebRTC): enable Center Stage for devices that support it ( Calin-Teodor 2026-07-08 11:27:56 +0300) 3537d38 build(deps-dev): bump @babel/core in /examples/GumTestApp_macOS (#1817) ( dependabot[bot] 2026-07-05 13:56:36 +0800) e8face3 dispose the peer connection after signalingState changes to "closed" (#1821) ( Philipp Hancke 2026-07-05 07:53:11 +0200) 0c63622 registerGlobals: don't set RTCRtpSender/RTCRtpReceiver twice ( Philipp Hancke 2026-07-04 11:25:23 +0200) c4ea2d3 build(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 (#1818) ( dependabot[bot] 2026-06-24 02:46:34 +0800) e5d8781 ios: fix trigger broadcast picker on new arch ( Calinteodor 2026-06-04 13:03:43 +0300) 3655418 fix: preserve facing mode in applyConstraints ( naveenkirugulige 2026-05-11 13:01:44 +0530) 7f851d5 build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#1773) ( dependabot[bot] 2026-05-10 01:28:56 +0800) fdc4771 build(deps-dev): bump @babel/plugin-transform-modules-systemjs (#1809) ( dependabot[bot] 2026-05-10 01:08:21 +0800) bc486df build(deps-dev): bump minimatch from 3.1.2 to 3.1.5 (#1791) ( dependabot[bot] 2026-04-30 14:40:03 +0800) f7b6382 ios: use runtime camera checks on simulator for camera availability ( Krzysztof Magiera 2026-04-14 12:31:59 +0200) 68ca776 pc: add RTCCertificate support ( xinfei.wu 2026-04-13 17:12:02 +0800) 643067f pc: add mediaConstraints on getDisplayMedia ( Frederic Luart 2026-04-09 16:45:22 +0200) a243f5e build(deps-dev): bump picomatch from 2.3.1 to 2.3.2 ( dependabot[bot] 2026-03-25 22:05:10 +0000) e36ddec build(deps-dev): bump flatted from 3.2.7 to 3.4.2 ( dependabot[bot] 2026-03-19 17:45:43 +0000) 43c665d style: make eslint happy ( wuxinfei 2026-03-13 20:57:18 +0800) 66e61ba chore: ignore src/vendor in eslint ( wuxinfei 2026-03-13 20:55:25 +0800) d6c7a40 chore: remove event-target-shim dependency from package-lock.json ( wuxinfei 2026-03-13 18:04:11 +0800) 495b416 refactor: update event-target-shim imports to use local vendor path ( wuxinfei 2026-03-13 17:55:32 +0800) c43189a refactor: replace defineEventAttribute with getter/setter methods for event attributes ( wuxinfei 2026-03-02 15:30:01 +0800) d2aa171 refactor: update event-target-shim imports to remove '/index' ( wuxinfei 2026-03-02 00:03:47 +0800) 5771fbd api: throw error in addIceCandidate when peer connection is closed ( naveenkirugulige 2026-03-04 16:22:50 +0530) 72f9dfd pc: add minBitrate to encoding parameters ( Martin Liu 2026-02-19 04:12:16 -0800) 5ba65ce android: fix ANR in getVideoTrackForStreamURL ( Saúl Ibarra Corretgé 2025-10-27 22:20:25 +0100) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 337752243..f6df96ee7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-webrtc", - "version": "124.0.7", + "version": "124.0.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-webrtc", - "version": "124.0.7", + "version": "124.0.8", "license": "MIT", "dependencies": { "base64-js": "1.5.1", diff --git a/package.json b/package.json index 28c2ab1bf..d6629c968 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-webrtc", - "version": "124.0.7", + "version": "124.0.8", "repository": { "type": "git", "url": "git+https://github.com/react-native-webrtc/react-native-webrtc.git" From cf6101cdfecff24ad2f57374dccd3026e5a11e4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:04:35 +0000 Subject: [PATCH 22/36] build(deps-dev): bump js-yaml from 4.2.0 to 4.3.0 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6df96ee7..9ae042df1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4213,9 +4213,9 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -8780,9 +8780,9 @@ "dev": true }, "js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "requires": { "argparse": "^2.0.1" From a312ef9a12cc8d79d741a28439ccc94ed621b6c7 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:33:50 +0900 Subject: [PATCH 23/36] feat(ios): configure the default audio session natively in the observer (#96) ## Summary Adds an iOS-only API that pushes the default audio-session policy to native once. When no custom JS handlers are registered, `willEnableEngine` and `didDisableEngine` now configure `AVAudioSession` on the native worker thread. This removes the JS round trip from the default path and the circular-wait risk described in #89. Custom JS handlers still take precedence and keep the bounded timeout. Native activation tracking handles normal stop, interruptions, and external deactivation. Switching between native and custom management during an active call remains unsupported. Switch while disconnected. ## Testing - Built for iOS device and simulator with Xcode 26.6 and WebRTC-SDK 144.7559.10 - Smoke tested on device with CallKit and RNCallKeep through connect, mic publish, mute, and disconnect - Observed no freeze, bridge-wait timeout, or native configuration failure - Confirmed the audio session returned inactive after teardown Current iOS CI failure is unrelated to this change. The React Native 0.71 example fails while compiling Yoga after `macos-latest` moved to macOS 26, before the native code from this PR is compiled. ## Before undrafting - [ ] Add a regression test for the #89 circular-wait shape - [ ] Repeat the device smoke test with the final decision logs Companion PR: livekit/client-sdk-react-native#434 --------- Co-authored-by: davidliu --- .github/workflows/ios_ci.yml | 2 +- ios/RCTWebRTC/AudioDeviceModuleObserver.h | 16 ++ ios/RCTWebRTC/AudioDeviceModuleObserver.m | 235 +++++++++++++++++- .../WebRTCModule+RTCAudioDeviceModule.m | 11 + src/AudioDeviceModule.ts | 83 +++++++ src/index.ts | 16 +- 6 files changed, 360 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios_ci.yml b/.github/workflows/ios_ci.yml index fef52a18e..0e5a69366 100644 --- a/.github/workflows/ios_ci.yml +++ b/.github/workflows/ios_ci.yml @@ -11,7 +11,7 @@ on: jobs: ios-compile: - runs-on: macos-latest + runs-on: macos-15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.h b/ios/RCTWebRTC/AudioDeviceModuleObserver.h index 1a3702549..22c070d77 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.h +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.h @@ -11,6 +11,10 @@ NS_ASSUME_NONNULL_BEGIN // immediately without a JS round trip, avoiding the deadlock window entirely. // Atomic because they are written on the JS thread (handler registration) and // read on the native audio thread (delegate callbacks). +// +// Warnings: +// - Clear willEnable/didDisable JS handlers as a pair while a policy is set; +// mixed ownership can activate a session that nothing releases. @property(atomic, assign) BOOL isEngineCreatedActive; @property(atomic, assign) BOOL isWillEnableEngineActive; @property(atomic, assign) BOOL isWillStartEngineActive; @@ -18,6 +22,18 @@ NS_ASSUME_NONNULL_BEGIN @property(atomic, assign) BOOL isDidDisableEngineActive; @property(atomic, assign) BOOL isWillReleaseEngineActive; +// Native default AVAudioSession policy. When non-nil and no JS willEnable/ +// didDisable handler is registered, applied on the worker thread. nil clears it. +// Shape: @{ @"recording": , @"playout": , @"deactivateOnStop": @(BOOL) } +// where is @{ @"audioCategory": str, @"audioMode": str, +// @"audioCategoryOptions": @[str...] }. +// +// Warnings: +// - Clearing a deactivateOnStop:NO policy while the engine is already stopped +// keeps the activation held until the next engine cycle. For an immediate +// release, stop under deactivateOnStop:YES before clearing. +@property(atomic, copy, nullable) NSDictionary *automaticAudioSessionConfig; + // Methods to receive results from JS. requestId echoes the id sent with the // corresponding event so stale responses from timed-out rounds can be dropped. - (void)resolveEngineCreatedWithRequestId:(NSInteger)requestId result:(NSInteger)result; diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.m b/ios/RCTWebRTC/AudioDeviceModuleObserver.m index 8b42cf11a..7b0fa37ae 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.m +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.m @@ -20,6 +20,11 @@ // outcome is deliberately preferred over the unrecoverable deadlock. static const int64_t kJSResponseTimeoutSeconds = 2; +// Returned from willEnable/didDisable when native automatic audio-session +// configuration fails, so libwebrtc rolls the engine operation back. Mirrors the +// SDK's kAudioEngineErrorFailedToConfigureAudioSession value. +static const NSInteger kAutomaticAudioSessionConfigError = -4100; + static os_log_t ADMObserverLog(void) { static os_log_t log; static dispatch_once_t onceToken; @@ -55,6 +60,13 @@ @interface AudioDeviceModuleObserver () @property(nonatomic, assign) NSInteger requestIdSeq; @property(nonatomic, assign) NSInteger awaitingRequestId; +// Whether the native auto-config path currently holds an +// RTCAudioSession activation it must later release. RTCAudioSession refcounts +// setActive, so a mismatched activate/release leaves the OS session stuck active. +// Touched only on the serial worker thread; atomic as insurance against future +// cross-thread reads. +@property(atomic, assign) BOOL autoSessionHoldsActivation; + @end @implementation AudioDeviceModuleObserver @@ -110,7 +122,7 @@ - (NSInteger)sendEventAndWaitWithName:(NSString *)eventName if (!isActive) { // No handler registered, proceed immediately without JS round trip. // This avoids the deadlock window entirely. - os_log_debug(ADMObserverLog(), "Skipping JS round-trip for %{public}@ (no handler registered)", eventName); + os_log(ADMObserverLog(), "Skipping JS round-trip for %{public}@ (no handler registered)", eventName); return 0; } @@ -185,6 +197,14 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willEnableEngine:(AVAudioEngine *)engine isPlayoutEnabled:(BOOL)isPlayoutEnabled isRecordingEnabled:(BOOL)isRecordingEnabled { + // With no custom JS handler registered, configure the audio session natively + // here instead of doing a JS round trip. This is the default path and avoids + // parking the audio worker thread on the JS thread entirely. A custom handler + // (isWillEnableEngineActive == YES) falls through to the bounded round trip. + if (!self.isWillEnableEngineActive && self.automaticAudioSessionConfig != nil) { + return [self applyAutomaticAudioSessionConfigForPlayout:isPlayoutEnabled recording:isRecordingEnabled]; + } + BOOL isActive = self.isWillEnableEngineActive; if (isActive) { @@ -276,6 +296,29 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didDisableEngine:(AVAudioEngine *)engine isPlayoutEnabled:(BOOL)isPlayoutEnabled isRecordingEnabled:(BOOL)isRecordingEnabled { + // Counterpart of willEnable - apply the native session policy (here the + // deactivate-on-stop edge) without a JS round trip when no custom handler + // is registered. Also entered with a nil policy while an activation hold is + // still outstanding, so a hold orphaned by clearing the policy is released + // at the next full stop instead of leaking. + if (!self.isDidDisableEngineActive && + (self.automaticAudioSessionConfig != nil || self.autoSessionHoldsActivation)) { + NSInteger result = [self applyAutomaticAudioSessionConfigForPlayout:isPlayoutEnabled + recording:isRecordingEnabled]; + if (result != 0) { + // didDisable fires after the engine's disable work has already run, so + // libwebrtc cannot roll this operation back. Propagating an error here + // (e.g. a failed reconfigure on a duplex to playout transition) would + // only desync its logical engine state from hardware that has already + // changed. Log and report the operation as completed. Only willEnable + // propagates configuration and activation errors, where the enable has + // not happened yet and a rollback is still meaningful. + os_log_error( + ADMObserverLog(), "Native auto-config: error %ld in didDisable treated as completed", (long)result); + } + return 0; + } + BOOL isActive = self.isDidDisableEngineActive; if (isActive) { @@ -419,6 +462,196 @@ - (void)resolveWillReleaseEngineWithRequestId:(NSInteger)requestId result:(NSInt semaphore:self.willReleaseEngineSemaphore]; } +#pragma mark - Native automatic audio session configuration + +// Applies the pushed audio-session policy on the worker thread: +// - configures on every active state, +// - activates when the session is not +// - optionally deactivates on stop, +// - returns a non-zero error code on failure so libwebrtc rolls the operation back. +// +// Activation is decided against RTCAudioSession's own state rather than a mirror +// of past engine states, so a policy re-push mid-call (setupIOSAudioManagement +// called again), a path switch, or an interruption cannot desync it: +// - session already active (previous hold, custom JS path, or the app) -> +// reconfigure only, never a second refcounted activation. +// - session inactive (fresh start, or the custom path deactivated it before a +// switch back to the native path) -> activate and take the hold. +- (NSInteger)applyAutomaticAudioSessionConfigForPlayout:(BOOL)isPlayoutEnabled recording:(BOOL)isRecordingEnabled { + NSDictionary *policy = self.automaticAudioSessionConfig; + BOOL nowActive = isPlayoutEnabled || isRecordingEnabled; + + if (policy == nil) { + // The policy was cleared while we still hold an activation (a setup was + // torn down mid-call, or a deactivateOnStop:NO policy was abandoned). + // Release the orphaned hold at the next full stop so the shared + // activation count stays balanced. No configuration is applied because + // the native path is disarmed. + if (!nowActive && self.autoSessionHoldsActivation) { + os_log(ADMObserverLog(), "Native auto-config: releasing orphaned activation hold"); + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + [session lockForConfiguration]; + NSError *releaseError = nil; + [session setActive:NO error:&releaseError]; + self.autoSessionHoldsActivation = NO; + [session unlockForConfiguration]; + if (releaseError != nil) { + os_log_error(ADMObserverLog(), + "Native auto-config: orphaned hold release failed (continuing): %{public}@", + releaseError.localizedDescription); + } + } + return 0; + } + + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + [session lockForConfiguration]; + + NSError *error = nil; + if (!nowActive) { + if (self.autoSessionHoldsActivation && [policy[@"deactivateOnStop"] boolValue]) { + os_log(ADMObserverLog(), "Native auto-config: deactivating audio session"); + NSError *deactivateError = nil; + [session setActive:NO error:&deactivateError]; + // RTCAudioSession decrements its activation count even when the OS + // session is already inactive (e.g. an interruption cleared it) or the + // call fails, so the hold is released in every outcome. + self.autoSessionHoldsActivation = NO; + if (deactivateError != nil) { + // Deliberately not propagated. By the time didDisable fires the + // engine's disable work is already done and the activation count is + // already released, so libwebrtc has nothing it could roll back. A + // non-zero return would only desync its logical engine state from + // hardware that is already stopped. + os_log_error(ADMObserverLog(), + "Native auto-config: deactivation failed (continuing): %{public}@", + deactivateError.localizedDescription); + } + } + } else { + // Recording uses the duplex (playAndRecord) config, while playout-only uses + // the playback config. Both are supplied by the SDK in automaticAudioSessionConfig. + NSDictionary *cfg = isRecordingEnabled ? policy[@"recording"] : policy[@"playout"]; + RTCAudioSessionConfiguration *rtcConfig = [RTCAudioSessionConfiguration webRTCConfiguration]; + if (cfg[@"audioCategory"] != nil) { + rtcConfig.category = [self avAudioSessionCategoryFromString:cfg[@"audioCategory"]]; + } + if (cfg[@"audioMode"] != nil) { + rtcConfig.mode = [self avAudioSessionModeFromString:cfg[@"audioMode"]]; + } + if (cfg[@"audioCategoryOptions"] != nil) { + rtcConfig.categoryOptions = [self avAudioSessionCategoryOptionsFromStrings:cfg[@"audioCategoryOptions"]]; + } + os_log(ADMObserverLog(), "Native auto-config: setting category %{public}@", rtcConfig.category); + [session setConfiguration:rtcConfig error:&error]; + if (error == nil && !session.isActive) { + BOOL hadHold = self.autoSessionHoldsActivation; + os_log(ADMObserverLog(), "Native auto-config: activating audio session"); + [session setActive:YES error:&error]; + if (error == nil) { + if (hadHold) { + // An interruption or an external deactivation (e.g. CallKit) + // cleared isActive while our activation count was still held, so + // the successful reactivation above stacked a second count onto + // the same hold. Drop the extra count. The drop is a pure + // decrement while the count sits above one, so the session stays + // active. + // + // Ordered activate-first deliberately. Releasing before + // reactivating would zero the count whenever the reactivation + // fails, and RTCAudioSession's interruption-end recovery + // deactivates outright at count zero. A failure must leave the + // prior hold untouched for that recovery to restore it. + os_log(ADMObserverLog(), "Native auto-config: dropping extra count after reactivating"); + NSError *dropError = nil; + [session setActive:NO error:&dropError]; + if (!session.isActive) { + // The drop deactivated, which means the held count had + // already been consumed by an external unmatched release and + // the drop just gave back the count the reactivation took. + // Reactivate and keep that single fresh count as the hold, + // so even a stale hold converges to a balanced state. + os_log(ADMObserverLog(), "Native auto-config: reactivating after dropping a stale hold"); + [session setActive:YES error:&error]; + } + } + if (error == nil) { + self.autoSessionHoldsActivation = YES; + } + } + } + } + + [session unlockForConfiguration]; + + if (error != nil) { + os_log_error(ADMObserverLog(), "Native auto-config failed: %{public}@", error.localizedDescription); + return kAutomaticAudioSessionConfigError; + } + + return 0; +} + +// TODO: this friendly-string -> AVAudioSession mapping mirrors AudioUtils in the +// LiveKit React Native SDK. Keep the accepted values in sync, or consolidate by +// pushing raw AVAudioSession values across the bridge instead. +- (NSString *)avAudioSessionCategoryFromString:(NSString *)value { + static NSDictionary *map; + static dispatch_once_t once; + dispatch_once(&once, ^{ + map = @{ + @"ambient" : AVAudioSessionCategoryAmbient, + @"soloAmbient" : AVAudioSessionCategorySoloAmbient, + @"playback" : AVAudioSessionCategoryPlayback, + @"record" : AVAudioSessionCategoryRecord, + @"playAndRecord" : AVAudioSessionCategoryPlayAndRecord, + @"multiRoute" : AVAudioSessionCategoryMultiRoute, + }; + }); + return map[value] ?: AVAudioSessionCategoryPlayAndRecord; +} + +- (NSString *)avAudioSessionModeFromString:(NSString *)value { + static NSDictionary *map; + static dispatch_once_t once; + dispatch_once(&once, ^{ + map = @{ + @"default" : AVAudioSessionModeDefault, + @"voiceChat" : AVAudioSessionModeVoiceChat, + @"videoChat" : AVAudioSessionModeVideoChat, + @"gameChat" : AVAudioSessionModeGameChat, + @"videoRecording" : AVAudioSessionModeVideoRecording, + @"measurement" : AVAudioSessionModeMeasurement, + @"moviePlayback" : AVAudioSessionModeMoviePlayback, + @"spokenAudio" : AVAudioSessionModeSpokenAudio, + @"voicePrompt" : AVAudioSessionModeVoicePrompt, + }; + }); + return map[value] ?: AVAudioSessionModeDefault; +} + +- (AVAudioSessionCategoryOptions)avAudioSessionCategoryOptionsFromStrings:(NSArray *)options { + AVAudioSessionCategoryOptions result = 0; + for (NSString *option in options) { + if ([option isEqualToString:@"mixWithOthers"]) { + result |= AVAudioSessionCategoryOptionMixWithOthers; + } else if ([option isEqualToString:@"duckOthers"]) { + result |= AVAudioSessionCategoryOptionDuckOthers; + } else if ([option isEqualToString:@"allowBluetooth"]) { + result |= AVAudioSessionCategoryOptionAllowBluetooth; + } else if ([option isEqualToString:@"allowBluetoothA2DP"]) { + result |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; + } else if ([option isEqualToString:@"allowAirPlay"]) { + result |= AVAudioSessionCategoryOptionAllowAirPlay; + } else if ([option isEqualToString:@"defaultToSpeaker"]) { + result |= AVAudioSessionCategoryOptionDefaultToSpeaker; + } else if ([option isEqualToString:@"interruptSpokenAudioAndMixWithOthers"]) { + result |= AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers; + } + } + return result; +} + @end NS_ASSUME_NONNULL_END diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index 0eb3d0aab..4405fbd1f 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -303,4 +303,15 @@ @implementation WebRTCModule (RTCAudioDeviceModule) return nil; } +#pragma mark - Automatic Audio Session Configuration + +// Pushes the native default audio-session policy (or nil to disable). When set, +// the observer configures the session natively in willEnable/didDisable instead +// of doing a JS round trip - removing the JS round trip from the default path. +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetAutomaticAudioSessionConfiguration + : (nullable NSDictionary *)config) { + self.audioDeviceModuleObserver.automaticAudioSessionConfig = config; + return nil; +} + @end diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts index 8d2ab676d..773a99482 100644 --- a/src/AudioDeviceModule.ts +++ b/src/AudioDeviceModule.ts @@ -25,11 +25,94 @@ export const AudioEngineAvailability = { }, } as const; +/** + * Accepted values mirror the native observer's friendly-name maps. Unknown + * values never reach the session: categories fall back to playAndRecord and + * modes to default, and the literal unions below reject them at compile time. + */ +export type AutomaticAppleAudioCategory = + | 'ambient' + | 'soloAmbient' + | 'playback' + | 'record' + | 'playAndRecord' + | 'multiRoute'; + +export type AutomaticAppleAudioMode = + | 'default' + | 'voiceChat' + | 'videoChat' + | 'gameChat' + | 'videoRecording' + | 'measurement' + | 'moviePlayback' + | 'spokenAudio' + | 'voicePrompt'; + +export type AutomaticAppleAudioCategoryOption = + | 'mixWithOthers' + | 'duckOthers' + | 'allowBluetooth' + | 'allowBluetoothA2DP' + | 'allowAirPlay' + | 'defaultToSpeaker' + | 'interruptSpokenAudioAndMixWithOthers'; + +/** + * Apple audio session configuration for a single engine state. Matches the + * AppleAudioConfiguration shape used by AudioSession.setAppleAudioConfiguration. + */ +export interface AutomaticAppleAudioConfiguration { + audioCategory?: AutomaticAppleAudioCategory; + audioMode?: AutomaticAppleAudioMode; + audioCategoryOptions?: AutomaticAppleAudioCategoryOption[]; +} + +/** + * Native default audio-session policy. When set, the native observer configures + * the AVAudioSession in willEnable/didDisable without a JS round trip. + * - `recording` is applied while recording is enabled, + * - `playout` is applied while only playout is enabled, + * - `deactivateOnStop` determines whether the session is deactivated when + * neither recording nor playout is enabled. + */ +export interface AutomaticAudioSessionConfiguration { + recording: AutomaticAppleAudioConfiguration; + playout: AutomaticAppleAudioConfiguration; + deactivateOnStop: boolean; +} + /** * Audio Device Module API for controlling audio devices and settings. * iOS/macOS only - will throw on Android. */ export class AudioDeviceModule { + /** + * Push (or clear with null) the native default audio-session configuration + * policy. When set, the native observer configures the session itself in + * willEnable/didDisable, removing the JS round trip from the default path. + * iOS only (including tvOS), a no-op elsewhere. The macOS build excludes + * the audio device module natives, so this must not reach the bridge there. + * + * Handler precedence is per hook: + * while a policy is set, custom willEnable/didDisable handlers must be + * registered or cleared as a pair, otherwise one regime can activate the + * session while the other never releases it. + * + * Clearing a `deactivateOnStop: false` policy while the engine is already + * stopped (playout and recording both disabled) keeps the session activation + * held until the next engine cycle, because no callback fires in between. + * For an immediate release, stop under `deactivateOnStop: true` before + * clearing. + */ + static setAutomaticAudioSessionConfiguration(config: AutomaticAudioSessionConfiguration | null): void { + if (Platform.OS !== 'ios' || !WebRTCModule) { + return; + } + + WebRTCModule.audioDeviceModuleSetAutomaticAudioSessionConfiguration(config); + } + /** * Start audio playback */ diff --git a/src/index.ts b/src/index.ts index 3f2634fbf..bf692a673 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,16 @@ if (WebRTCModule === null) { }`); } -import { AudioDeviceModule, AudioEngineMuteMode, AudioEngineAvailability } from './AudioDeviceModule'; +import { + AudioDeviceModule, + AudioEngineMuteMode, + AudioEngineAvailability, + type AutomaticAudioSessionConfiguration, + type AutomaticAppleAudioConfiguration, + type AutomaticAppleAudioCategory, + type AutomaticAppleAudioMode, + type AutomaticAppleAudioCategoryOption, +} from './AudioDeviceModule'; import { audioDeviceModuleEvents } from './AudioDeviceModuleEvents'; import { setupNativeEvents } from './EventEmitter'; import Logger from './Logger'; @@ -87,6 +96,11 @@ export { AudioDeviceModule, AudioEngineMuteMode, AudioEngineAvailability, + type AutomaticAudioSessionConfiguration, + type AutomaticAppleAudioConfiguration, + type AutomaticAppleAudioCategory, + type AutomaticAppleAudioMode, + type AutomaticAppleAudioCategoryOption, audioDeviceModuleEvents, }; From 2edc2f064418a1924a13e0a6d86393f6e01735f8 Mon Sep 17 00:00:00 2001 From: davidliu Date: Thu, 23 Jul 2026 22:49:44 +0900 Subject: [PATCH 24/36] release: 144.1.2 (#97) a312ef9 feat(ios): configure the default audio session natively in the observer (#96) ( Hiroshi Horie 2026-07-23 22:33:50 +0900) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed101483e..23c735dcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@livekit/react-native-webrtc", - "version": "144.1.1", + "version": "144.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@livekit/react-native-webrtc", - "version": "144.1.1", + "version": "144.1.2", "license": "MIT", "dependencies": { "base64-js": "1.5.1", diff --git a/package.json b/package.json index 5235e6a3e..0d19fc303 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@livekit/react-native-webrtc", - "version": "144.1.1", + "version": "144.1.2", "repository": { "type": "git", "url": "git+https://github.com/livekit/react-native-webrtc.git" From 0483cc8220d2fd22201612292e21d74eff597ca9 Mon Sep 17 00:00:00 2001 From: HanSeonWoo Date: Tue, 28 Jul 2026 20:09:06 +0900 Subject: [PATCH 25/36] android: release intermediate video effect frames --- .../WebRTCModule/videoEffects/VideoEffectProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java b/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java index 925a84056..e2ca6f241 100644 --- a/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java +++ b/android/src/main/java/com/oney/WebRTCModule/videoEffects/VideoEffectProcessor.java @@ -43,17 +43,17 @@ public void onFrameCaptured(VideoFrame frame) { frame.retain(); VideoFrame outputFrame = frame; for (VideoFrameProcessor processor : this.videoFrameProcessors) { - outputFrame = processor.process(outputFrame, textureHelper); + VideoFrame inputFrame = outputFrame; + outputFrame = processor.process(inputFrame, textureHelper); + inputFrame.release(); if (outputFrame == null) { mSink.onFrame(frame); - frame.release(); return; } } mSink.onFrame(outputFrame); outputFrame.release(); - frame.release(); } } From 78d3094dd869551edd818f0acb2a1e332de63fd0 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Wed, 29 Jul 2026 08:17:31 -0500 Subject: [PATCH 26/36] fix(android): guard against duplicate onActivityResult in screen capture Some hosts forward Activity results to every registered ActivityEventListener more than once (react-native-navigation is a common example). During the getDisplayMedia() screen-capture permission flow this makes onActivityResult fire twice for a single request: the first pass consumes displayMediaPromise (resolving it and nulling it) and the second pass calls resolve()/reject() on the now-null promise, crashing with a NullPointerException. It also spins up a second MediaProjection virtual display. Add null-guards so a duplicate dispatch is ignored: one at the top of the PERMISSION_REQUEST_CODE branch (covers the cancel path, which nulls the promise synchronously) and one at the top of createScreenStream() (covers the success path, since the single-threaded executor runs the duplicated callbacks in order). --- .../com/oney/WebRTCModule/GetUserMediaImpl.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java index e15cdb88a..e9f5e3bc7 100644 --- a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java +++ b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java @@ -75,6 +75,15 @@ class GetUserMediaImpl { public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { super.onActivityResult(activity, requestCode, resultCode, data); if (requestCode == PERMISSION_REQUEST_CODE) { + // Guard against a duplicate onActivityResult dispatch. Some hosts (e.g. + // react-native-navigation) forward the activity result to every registered + // ActivityEventListener more than once, so this callback can fire twice for a + // single getDisplayMedia() request. The first pass consumes displayMediaPromise; + // a second pass would call reject()/resolve() on a null promise and crash. + if (displayMediaPromise == null) { + return; + } + if (resultCode != Activity.RESULT_OK) { displayMediaPromise.reject("DOMException", "NotAllowedError"); displayMediaPromise = null; @@ -357,6 +366,14 @@ public void run() { } private void createScreenStream() { + // A duplicate onActivityResult dispatch (see onActivityResult above) can schedule this more + // than once. The single-threaded executor runs them in order, so by the time a duplicate + // runs the first has already consumed displayMediaPromise. Bail out instead of dereferencing + // a null promise or creating a second screen stream. + if (displayMediaPromise == null) { + return; + } + VideoTrack track = createScreenTrack(); if (track == null) { From 014a8cf07a9d6550879e382b60bf168175c6ba60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Fri, 31 Jul 2026 11:59:46 +0200 Subject: [PATCH 27/36] build: ship the vendored declarations in the typescript build The typescript target builds with `tsc --emitDeclarationOnly`, and tsc never emits output for .d.ts inputs, so src/vendor/event-target-shim/index.d.ts never made it into lib/typescript. Every `import from './vendor/event-target-shim'` in the shipped declarations then failed to resolve, which silently stripped the EventTarget members off our classes for consumers using the declaration files: error TS2339: Property 'addEventListener' does not exist on type 'RTCPeerConnection'. skipLibCheck (on by default in react-native's tsconfig) hides the resolution error itself, so it only ever surfaced at the call sites. The commonjs and module targets mishandle the same file in the other direction: they compile every source file and rewrite the extension to .js, emitting an index.d.js which holds no code and which nothing imports. Fix both in a postbuild step, since bob 0.18.2 can neither copy extra files into the typescript output nor exclude files from the babel targets. Fixes #1830 --- package.json | 2 +- tools/postbuild.mjs | 67 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tools/postbuild.mjs diff --git a/package.json b/package.json index d6629c968..2ac3de8ec 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "scripts": { "lint": "eslint --max-warnings 0 . && tsc --noEmit", "lintfix": "eslint --max-warnings 0 --fix . && tsc --noEmit", - "prepare": "husky install && bob build", + "prepare": "husky install && bob build && node tools/postbuild.mjs", "format": "tools/format.sh" }, "bugs": { diff --git a/tools/postbuild.mjs b/tools/postbuild.mjs new file mode 100644 index 000000000..3890cb388 --- /dev/null +++ b/tools/postbuild.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +/** + * Fixes up the bob build output for the declaration files which are shipped as-is + * (currently the ones for the vendored event-target-shim). bob mishandles them in + * both directions, so for every .d.ts file in src/ we: + * + * - Copy it into the typescript output. That target builds with + * `tsc --emitDeclarationOnly`, and tsc never emits output for .d.ts inputs, so the + * relative imports pointing at them fail to resolve for consumers using the + * declaration files, which silently strips the EventTarget members off our classes. + * + * - Delete the empty module babel emitted for it in the commonjs and module outputs. + * Those targets compile every source file and rewrite the extension to .js, turning + * index.d.ts into an index.d.js which holds no code and which nothing imports. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const root = path.resolve(import.meta.dirname, '..'); +const source = path.join(root, 'src'); +const output = path.join(root, 'lib'); +const declarationsOutput = path.join(output, 'typescript'); +const compiledOutputs = [ path.join(output, 'commonjs'), path.join(output, 'module') ]; + +/** + * Finds every declaration file below the given directory, as paths relative to it. + */ +function findDeclarations(directory, prefix = '') { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const entryPath = path.join(prefix, entry.name); + + if (entry.isDirectory()) { + return findDeclarations(path.join(directory, entry.name), entryPath); + } + + return entry.name.endsWith('.d.ts') ? [ entryPath ] : []; + }); +} + +if (!fs.existsSync(output)) { + console.error(`${path.relative(root, output)} not found, run "bob build" first.`); + process.exit(1); +} + +for (const declaration of findDeclarations(source)) { + const target = path.join(declarationsOutput, declaration); + + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(path.join(source, declaration), target); + + console.info(`Copied src/${declaration} -> ${path.relative(root, target)}`); + + for (const compiledOutput of compiledOutputs) { + const compiled = path.join(compiledOutput, declaration.replace(/\.ts$/, '.js')); + + for (const artifact of [ compiled, `${compiled}.map` ]) { + if (fs.existsSync(artifact)) { + fs.rmSync(artifact); + + console.info(`Removed ${path.relative(root, artifact)}`); + } + } + } +} From 9a5a928454ae4165d2b4e9c810f8ae867dd9042e Mon Sep 17 00:00:00 2001 From: Philipp Hancke Date: Sun, 5 Jul 2026 07:53:11 +0200 Subject: [PATCH 28/36] dispose the peer connection after signalingState changes to "closed" (#1821) When close() is called, libwebrtc emits the observer state changes in the order iceConnectionState, connectionState, then signalingState (all "closed"). react-native-webrtc was tearing down its listeners on connectionState === "closed", which dropped the subsequent signalingState "closed" event, leaving signalingState stuck at its previous value. Move the teardown to the signalingState === "closed" handler so every final state change is applied before the listeners are removed. See PeerConnection::Close(): https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/pc/peer_connection.cc;l=1818;drc=4e0c079a2b24c7ec577949494e94ba6f5bf264e4 --- src/RTCPeerConnection.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/RTCPeerConnection.ts b/src/RTCPeerConnection.ts index 36dca8818..d03444968 100644 --- a/src/RTCPeerConnection.ts +++ b/src/RTCPeerConnection.ts @@ -742,13 +742,6 @@ export default class RTCPeerConnection extends EventTarget { @@ -759,6 +752,13 @@ export default class RTCPeerConnection extends EventTarget Date: Sat, 4 Jul 2026 11:25:23 +0200 Subject: [PATCH 29/36] registerGlobals: don't set RTCRtpSender/RTCRtpReceiver twice in particular when the first instance is wrongly setting the sender to the receiver --- src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6d48b9620..6b3499629 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,8 +92,6 @@ function registerGlobals(): void { global.RTCIceCandidate = RTCIceCandidate; global.RTCCertificate = RTCCertificate; global.RTCPeerConnection = RTCPeerConnection; - global.RTCRtpReceiver = RTCRtpReceiver; - global.RTCRtpSender = RTCRtpReceiver; global.RTCSessionDescription = RTCSessionDescription; global.MediaStream = MediaStream; global.MediaStreamTrack = MediaStreamTrack; From 7694cadc7f7d527f04cb1296a41eea352b663028 Mon Sep 17 00:00:00 2001 From: naveenkirugulige Date: Mon, 11 May 2026 13:01:44 +0530 Subject: [PATCH 30/36] fix: preserve facing mode in applyConstraints --- src/MediaStreamTrack.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/MediaStreamTrack.ts b/src/MediaStreamTrack.ts index 9f84ddc80..d17f9aab5 100644 --- a/src/MediaStreamTrack.ts +++ b/src/MediaStreamTrack.ts @@ -248,6 +248,11 @@ export default class MediaStreamTrack extends EventTarget Date: Wed, 29 Jul 2026 08:17:31 -0500 Subject: [PATCH 31/36] fix(android): guard against duplicate onActivityResult in screen capture Some hosts forward Activity results to every registered ActivityEventListener more than once (react-native-navigation is a common example). During the getDisplayMedia() screen-capture permission flow this makes onActivityResult fire twice for a single request: the first pass consumes displayMediaPromise (resolving it and nulling it) and the second pass calls resolve()/reject() on the now-null promise, crashing with a NullPointerException. It also spins up a second MediaProjection virtual display. Add null-guards so a duplicate dispatch is ignored: one at the top of the PERMISSION_REQUEST_CODE branch (covers the cancel path, which nulls the promise synchronously) and one at the top of createScreenStream() (covers the success path, since the single-threaded executor runs the duplicated callbacks in order). --- .../oney/WebRTCModule/GetUserMediaImpl.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java index b6985f7d8..e06d8c3e4 100644 --- a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java +++ b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java @@ -103,6 +103,15 @@ public void onServiceDisconnected(ComponentName name) { public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { super.onActivityResult(activity, requestCode, resultCode, data); if (requestCode == PERMISSION_REQUEST_CODE) { + // Guard against a duplicate onActivityResult dispatch. Some hosts (e.g. + // react-native-navigation) forward the activity result to every registered + // ActivityEventListener more than once, so this callback can fire twice for a + // single getDisplayMedia() request. The first pass consumes displayMediaPromise; + // a second pass would call reject()/resolve() on a null promise and crash. + if (displayMediaPromise == null) { + return; + } + if (resultCode != Activity.RESULT_OK) { displayMediaPromise.reject("DOMException", "NotAllowedError"); displayMediaPromise = null; @@ -429,12 +438,19 @@ public void run() { } private void createScreenStream() { + // A duplicate dispatch (see onActivityResult above) can schedule this more than once: a + // repeated launch() rebinds the service, so onServiceConnected can fire twice. The + // single-threaded executor runs them in order, so by the time a duplicate runs the first has + // already consumed displayMediaPromise. Bail out instead of dereferencing a null promise or + // creating a second screen stream. + if (displayMediaPromise == null) { + return; + } + final PeerConnectionFactoryProvider factoryProvider = displayMediaFactory; if (factoryProvider == null || factoryProvider.isDisposed()) { - if (displayMediaPromise != null) { - displayMediaPromise.reject("ERR_MODULE_DISPOSED", "WebRTCModule disposed during getDisplayMedia"); - displayMediaPromise = null; - } + displayMediaPromise.reject("ERR_MODULE_DISPOSED", "WebRTCModule disposed during getDisplayMedia"); + displayMediaPromise = null; displayMediaFactory = null; return; } From 5785bcae3f9099ee42537095aa59ac1106c92812 Mon Sep 17 00:00:00 2001 From: Calinteodor Date: Thu, 4 Jun 2026 13:03:43 +0300 Subject: [PATCH 32/36] ios: fix trigger broadcast picker on new arch --- .../ScreenCapturePickerViewManager.m | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/ios/RCTWebRTC/ScreenCapturePickerViewManager.m b/ios/RCTWebRTC/ScreenCapturePickerViewManager.m index d5c2101fe..91e0c4c2c 100644 --- a/ios/RCTWebRTC/ScreenCapturePickerViewManager.m +++ b/ios/RCTWebRTC/ScreenCapturePickerViewManager.m @@ -1,6 +1,6 @@ #if TARGET_OS_IOS -#import +#import #import #import "ScreenCapturePickerViewManager.h" @@ -30,29 +30,28 @@ - (NSString *)preferredExtension { } RCT_EXPORT_METHOD(show : (nonnull NSNumber *)reactTag) { - [self.bridge.uiManager - addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - id view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RPSystemBroadcastPickerView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting " - @"RPSystemBroadcastPickerView, got: %@", - view); - } else { - // Simulate a click - UIButton *btn = nil; - - for (UIView *subview in ((RPSystemBroadcastPickerView *)view).subviews) { - if ([subview isKindOfClass:[UIButton class]]) { - btn = (UIButton *)subview; - } - } - if (btn != nil) { - [btn sendActionsForControlEvents:UIControlEventTouchUpInside]; - } else { - RCTLogError(@"RPSystemBroadcastPickerView button not found"); - } + dispatch_async(dispatch_get_main_queue(), ^{ + RPSystemBroadcastPickerView *picker = self->_broadcastPickerView; + if (![picker isKindOfClass:[RPSystemBroadcastPickerView class]]) { + RCTLogError(@"Invalid broadcast picker view, expecting " + @"RPSystemBroadcastPickerView, got: %@", + picker); + return; + } + + UIButton *btn = nil; + + for (UIView *subview in picker.subviews) { + if ([subview isKindOfClass:[UIButton class]]) { + btn = (UIButton *)subview; } - }]; + } + if (btn != nil) { + [btn sendActionsForControlEvents:UIControlEventTouchUpInside]; + } else { + RCTLogError(@"RPSystemBroadcastPickerView button not found"); + } + }); } @end From e9eaa8f14f4b15525f9c031b2ce037ef6c81d5ca Mon Sep 17 00:00:00 2001 From: Calin-Teodor Date: Wed, 8 Jul 2026 11:27:56 +0300 Subject: [PATCH 33/36] ios(RCTWebRTC): enable Center Stage for devices that support it --- ios/RCTWebRTC/VideoCaptureController.m | 59 +++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index 08838104f..585140c56 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -52,9 +52,42 @@ - (void)startCapture { return; } + // Enable Center Stage when the device supports it; cooperative with Control Center. + if (@available(iOS 16.0, *)) { + BOOL centerStageSupported = NO; + for (AVCaptureDeviceFormat *fmt in [RTCCameraVideoCapturer supportedFormatsForDevice:self.device]) { + if (fmt.isCenterStageSupported) { + centerStageSupported = YES; + break; + } + } + if (centerStageSupported) { + AVCaptureDevice.centerStageControlMode = AVCaptureCenterStageControlModeCooperative; + AVCaptureDevice.centerStageEnabled = YES; + } else if (AVCaptureDevice.isCenterStageEnabled) { + AVCaptureDevice.centerStageEnabled = NO; + } + } + AVCaptureDeviceFormat *format = [self selectFormatForDevice:self.device withTargetWidth:self.width withTargetHeight:self.height]; + + // On multi cam devices selectFormatForDevice also requires format.multiCamSupported, and that set + // can be disjoint from the Center Stage one, leaving nothing to pick. Give up Center Stage rather + // than the capture: the control mode is cooperative above, so we are allowed to turn it off, and + // doing so lets AVFoundation accept a non-Center-Stage activeFormat. + if (@available(iOS 16.0, *)) { + if (!format && AVCaptureDevice.isCenterStageEnabled) { + RCTLogWarn(@"[VideoCaptureController] No Center Stage format for device %@, disabling Center Stage", + self.device); + AVCaptureDevice.centerStageEnabled = NO; + format = [self selectFormatForDevice:self.device + withTargetWidth:self.width + withTargetHeight:self.height]; + } + } + if (!format) { RCTLogWarn(@"[VideoCaptureController] No valid formats for device %@", self.device); @@ -63,6 +96,17 @@ - (void)startCapture { self.selectedFormat = format; + // Clamp fps to the range Center Stage allows for this format. + int fps = self.frameRate; + if (@available(iOS 16.0, *)) { + if (AVCaptureDevice.isCenterStageEnabled) { + AVFrameRateRange *csRange = format.videoFrameRateRangeForCenterStage; + if (csRange) { + fps = MAX((int)csRange.minFrameRate, MIN(fps, (int)csRange.maxFrameRate)); + } + } + } + AVCaptureSession *session = self.capturer.captureSession; if (@available(iOS 16.0, *)) { BOOL enable = self.enableMultitaskingCameraAccess; @@ -84,7 +128,7 @@ - (void)startCapture { __weak VideoCaptureController *weakSelf = self; [self.capturer startCaptureWithDevice:self.device format:format - fps:self.frameRate + fps:fps completionHandler:^(NSError *err) { if (err) { RCTLogError(@"[VideoCaptureController] Error starting capture: %@", err); @@ -296,6 +340,11 @@ - (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device AVCaptureDeviceFormat *selectedFormat = nil; int currentDiff = INT_MAX; + BOOL centerStageEnabled = NO; + if (@available(iOS 16.0, *)) { + centerStageEnabled = AVCaptureDevice.isCenterStageEnabled; + } + for (AVCaptureDeviceFormat *format in formats) { // Only use multi cam formats when on multi cam supported devices. if (@available(iOS 13.0, macOS 14.0, tvOS 17.0, *)) { @@ -304,6 +353,14 @@ - (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device } } + // Center Stage only permits supported formats. + if (@available(iOS 16.0, *)) { + if (centerStageEnabled && !format.isCenterStageSupported) { + continue; + } + } + + CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription); FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription); int diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height); From 4754c5fdeabe7bd05424f8cfdd07e942b523919c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Fri, 31 Jul 2026 11:59:46 +0200 Subject: [PATCH 34/36] build: ship the vendored declarations in the typescript build The typescript target builds with `tsc --emitDeclarationOnly`, and tsc never emits output for .d.ts inputs, so src/vendor/event-target-shim/index.d.ts never made it into lib/typescript. Every `import from './vendor/event-target-shim'` in the shipped declarations then failed to resolve, which silently stripped the EventTarget members off our classes for consumers using the declaration files: error TS2339: Property 'addEventListener' does not exist on type 'RTCPeerConnection'. skipLibCheck (on by default in react-native's tsconfig) hides the resolution error itself, so it only ever surfaced at the call sites. The commonjs and module targets mishandle the same file in the other direction: they compile every source file and rewrite the extension to .js, emitting an index.d.js which holds no code and which nothing imports. Fix both in a postbuild step, since bob 0.18.2 can neither copy extra files into the typescript output nor exclude files from the babel targets. Fixes #1830 --- package.json | 2 +- tools/postbuild.mjs | 67 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tools/postbuild.mjs diff --git a/package.json b/package.json index 677cafd04..db8e7f5db 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "build": "bob build", "lint": "eslint --max-warnings 0 . && tsc --noEmit", "lintfix": "eslint --max-warnings 0 --fix . && tsc --noEmit", - "prepare": "husky install && bob build", + "prepare": "husky install && bob build && node tools/postbuild.mjs", "format": "tools/format.sh", "semantic-release": "semantic-release" }, diff --git a/tools/postbuild.mjs b/tools/postbuild.mjs new file mode 100644 index 000000000..3890cb388 --- /dev/null +++ b/tools/postbuild.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +/** + * Fixes up the bob build output for the declaration files which are shipped as-is + * (currently the ones for the vendored event-target-shim). bob mishandles them in + * both directions, so for every .d.ts file in src/ we: + * + * - Copy it into the typescript output. That target builds with + * `tsc --emitDeclarationOnly`, and tsc never emits output for .d.ts inputs, so the + * relative imports pointing at them fail to resolve for consumers using the + * declaration files, which silently strips the EventTarget members off our classes. + * + * - Delete the empty module babel emitted for it in the commonjs and module outputs. + * Those targets compile every source file and rewrite the extension to .js, turning + * index.d.ts into an index.d.js which holds no code and which nothing imports. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const root = path.resolve(import.meta.dirname, '..'); +const source = path.join(root, 'src'); +const output = path.join(root, 'lib'); +const declarationsOutput = path.join(output, 'typescript'); +const compiledOutputs = [ path.join(output, 'commonjs'), path.join(output, 'module') ]; + +/** + * Finds every declaration file below the given directory, as paths relative to it. + */ +function findDeclarations(directory, prefix = '') { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const entryPath = path.join(prefix, entry.name); + + if (entry.isDirectory()) { + return findDeclarations(path.join(directory, entry.name), entryPath); + } + + return entry.name.endsWith('.d.ts') ? [ entryPath ] : []; + }); +} + +if (!fs.existsSync(output)) { + console.error(`${path.relative(root, output)} not found, run "bob build" first.`); + process.exit(1); +} + +for (const declaration of findDeclarations(source)) { + const target = path.join(declarationsOutput, declaration); + + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(path.join(source, declaration), target); + + console.info(`Copied src/${declaration} -> ${path.relative(root, target)}`); + + for (const compiledOutput of compiledOutputs) { + const compiled = path.join(compiledOutput, declaration.replace(/\.ts$/, '.js')); + + for (const artifact of [ compiled, `${compiled}.map` ]) { + if (fs.existsSync(artifact)) { + fs.rmSync(artifact); + + console.info(`Removed ${path.relative(root, artifact)}`); + } + } + } +} From 6aadb835ee647cee868ee7d530096f940a1befe4 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Thu, 13 Aug 2026 11:18:13 +0200 Subject: [PATCH 35/36] style: clang-format the Center Stage changes --- ios/RCTWebRTC/VideoCaptureController.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index 585140c56..61cc7f6e7 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -82,9 +82,7 @@ - (void)startCapture { RCTLogWarn(@"[VideoCaptureController] No Center Stage format for device %@, disabling Center Stage", self.device); AVCaptureDevice.centerStageEnabled = NO; - format = [self selectFormatForDevice:self.device - withTargetWidth:self.width - withTargetHeight:self.height]; + format = [self selectFormatForDevice:self.device withTargetWidth:self.width withTargetHeight:self.height]; } } @@ -360,7 +358,6 @@ - (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device } } - CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription); FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription); int diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height); From aefbef7eabc245580b335c7080c61dc6811bde7b Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Thu, 13 Aug 2026 12:53:35 +0200 Subject: [PATCH 36/36] fix: don't mutate the caller's constraints in applyConstraints The cherry-picked facingMode preservation assigned straight onto the caller's constraints object, which threw on a frozen input and leaked the injected facingMode back to the caller. It also stored that same object in _constraints, so getConstraints() aliased something the caller could mutate afterwards. Apply the fallback to a copy instead. Observable behaviour is unchanged: the undefined case still normalizes as 'video: true' and still records {}. --- src/MediaStreamTrack.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/MediaStreamTrack.ts b/src/MediaStreamTrack.ts index d17f9aab5..ed3743b86 100644 --- a/src/MediaStreamTrack.ts +++ b/src/MediaStreamTrack.ts @@ -248,15 +248,19 @@ export default class MediaStreamTrack extends EventTarget