diff --git a/package-lock.json b/package-lock.json index 6fcd446..ebeb561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@yume-chan/adb": "^2.5.1", "@yume-chan/adb-scrcpy": "^2.3.2", "@yume-chan/adb-server-node-tcp": "^2.5.2", - "@yume-chan/scrcpy-decoder-tinyh264": "^2.1.0", "@yume-chan/scrcpy-decoder-webcodecs": "^2.5.0", "@yume-chan/stream-extra": "2.1.0", "autoprefixer": "^10.4.20", @@ -1938,6 +1937,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1951,6 +1953,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1964,6 +1969,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1977,6 +1985,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1990,6 +2001,9 @@ "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2003,6 +2017,9 @@ "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2016,6 +2033,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2029,6 +2049,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2042,6 +2065,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2055,6 +2081,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2068,6 +2097,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2081,6 +2113,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2094,6 +2129,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2383,9 +2421,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", - "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3522,7 +3560,6 @@ "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -3543,12 +3580,11 @@ } }, "node_modules/bare-os": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz", - "integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -3559,7 +3595,6 @@ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } @@ -3570,7 +3605,6 @@ "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "streamx": "^2.21.0", "teex": "^1.0.1" @@ -3594,7 +3628,6 @@ "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -3674,6 +3707,31 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3930,9 +3988,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", "funding": [ { "type": "opencollective", @@ -4540,9 +4598,9 @@ } }, "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4727,53 +4785,12 @@ } }, "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==", + "license": "BSD", "dependencies": { - "safe-buffer": "~5.1.0" + "readable-stream": "~1.1.9" } }, "node_modules/ee-first": { @@ -4783,9 +4800,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "license": "ISC" }, "node_modules/emittery": { @@ -6400,9 +6417,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "license": "ISC" }, "node_modules/for-each": { @@ -6460,9 +6477,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -6773,9 +6790,9 @@ } }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -7951,9 +7968,9 @@ } }, "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "license": "MIT" }, "node_modules/isexe": { @@ -8789,6 +8806,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9325,39 +9348,6 @@ "duplexer2": "0.0.2" } }, - "node_modules/multipipe/node_modules/duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==", - "license": "BSD", - "dependencies": { - "readable-stream": "~1.1.9" - } - }, - "node_modules/multipipe/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/multipipe/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/multipipe/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "license": "MIT" - }, "node_modules/multistream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", @@ -9383,6 +9373,31 @@ "readable-stream": "^3.6.0" } }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/multistream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/mute-stream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", @@ -9517,9 +9532,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/normalize-path": { @@ -10019,9 +10034,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -10209,6 +10224,31 @@ "dev": true, "license": "ISC" }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/prebuild-install/node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -10339,9 +10379,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -10601,18 +10641,15 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, "node_modules/readdirp": { @@ -10995,6 +11032,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -11032,6 +11076,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -11615,14 +11666,10 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" }, "node_modules/string-length": { "version": "4.0.2", @@ -12021,9 +12068,9 @@ } }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -12038,9 +12085,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", "dev": true, "license": "MIT", "dependencies": { @@ -12053,13 +12100,14 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -12095,7 +12143,6 @@ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "streamx": "^2.12.5" } @@ -12741,9 +12788,9 @@ } }, "node_modules/typescript-eslint/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -12863,6 +12910,56 @@ "node-int64": "^0.4.0" } }, + "node_modules/unzipper/node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -13155,6 +13252,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", diff --git a/package.json b/package.json index ede6ef0..bab55ad 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "@yume-chan/adb": "^2.5.1", "@yume-chan/adb-scrcpy": "^2.3.2", "@yume-chan/adb-server-node-tcp": "^2.5.2", - "@yume-chan/scrcpy-decoder-tinyh264": "^2.1.0", "@yume-chan/scrcpy-decoder-webcodecs": "^2.5.0", "@yume-chan/stream-extra": "2.1.0", "autoprefixer": "^10.4.20", diff --git a/src/api/android/scrcpy/ScrcpyServer.ts b/src/api/android/scrcpy/ScrcpyServer.ts index 809ce7c..cb6b145 100644 --- a/src/api/android/scrcpy/ScrcpyServer.ts +++ b/src/api/android/scrcpy/ScrcpyServer.ts @@ -3,9 +3,8 @@ import path from "path"; import { ReadableStream } from "@yume-chan/stream-extra"; import { Adb } from "@yume-chan/adb"; -import { DefaultServerPath, ScrcpyMediaStreamPacket, ScrcpyCodecOptions } from "@yume-chan/scrcpy"; +import { DefaultServerPath, ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy"; import { AdbScrcpyClient, AdbScrcpyExitedError, AdbScrcpyOptions3_3_3 } from "@yume-chan/adb-scrcpy"; -import { TinyH264Decoder } from "@yume-chan/scrcpy-decoder-tinyh264"; import uWS, { TemplatedApp } from "uWebSockets.js"; import {getLogger, Logger} from "@logtape/logtape"; @@ -14,8 +13,6 @@ import { AdbManager } from "../adb/AdbManager.ts"; // Override the log function const logger = getLogger(["android", "ScrcpyServer"]); -const H264Capabilities = TinyH264Decoder.capabilities.h264; - // Starts with optimistic settings let useH265: boolean = true; // Switch to true if at least 1 client doesn't h265 @@ -36,25 +33,24 @@ function watchList(list: T[], onChange: () => void): T[] { export class ScrcpyServer { // ======================= // WebSocket - private wsServer!: TemplatedApp; - private wsClients: Set>; - private maxBackpressure: number = 8 * 1024 * 1024; // 8 MB per socket + private readonly wsServer!: TemplatedApp; + private readonly wsClients: Set>; // control channel: codec negotiation + stream_available announcements + private readonly streamClients: Map>>; // per-device data sockets, keyed by device IP + private readonly activeStreams: Set; // device IPs with a live scrcpy session (used to validate /stream/:id upgrades) + private readonly scrcpyClientsByIp: Map>>; // for triggering config reset on new device socket + + private readonly maxBackpressure: number = 8 * 1024 * 1024; // 8 MB private scrcpyClients: AdbScrcpyClient>[] = watchList([], () => { logger.debug("Scrcpy clients changed, restarting all video streams"); this.resentAllConfigPackage(); - });//[]; + }); // ======================= // Scrcpy server - declare server: Buffer; //ArrayBuffer; - - // ======================= - // Scrcpy stream - //@ts-expect-error this value is used line 321 - private scrcpyStreamConfig!: string; + declare server: Buffer; - private adbManager!: AdbManager; + private readonly adbManager!: AdbManager; constructor(adbManager: AdbManager) { // Set global variables @@ -64,12 +60,15 @@ export class ScrcpyServer { this.adbManager = adbManager; this.wsClients = new Set>(); + this.streamClients = new Map(); + this.activeStreams = new Set(); + this.scrcpyClientsByIp = new Map(); const host = process.env.WEB_APPLICATION_HOST || '0.0.0.0'; const port = parseInt(process.env.VIDEO_WS_PORT || '8082', 10); try { - this.wsServer = uWS.App(); //new WebSocketServer({ host, port }); + this.wsServer = uWS.App(); logger.info(`Creating video stream server on: ws://${host}:${port}`); } catch (e) { logger.error('Failed to create a new websocket {e}', { e }); @@ -94,10 +93,12 @@ export class ScrcpyServer { open: (ws) => { this.wsClients.add(ws); - logger.debug("Web view connected"); + logger.debug("Control client connected"); - // Restart every video stream on new client to have them all sync - this.resentAllConfigPackage(true).then(() => logger.debug("Streams restarted for new client connected")); + // Announce all currently active streams so the client can open their per-device data sockets + for (const ip of this.activeStreams) { + ws.send(JSON.stringify({ type: "stream_available", streamId: ip }), false, false); + } }, drain: (ws) => { @@ -126,6 +127,11 @@ export class ScrcpyServer { // Reset video streams if codec changed ! if (previousCodec != useH265) { logger.warn(`Restarting streams with new codec (${useH265 ? "h265" : "h264"})`); + // Clear active streams immediately so control clients that reconnect + // during the transition don't receive stale stream_available announcements. + // The exited handlers on old clients will now find no matching entry to delete. + this.activeStreams.clear(); + this.scrcpyClientsByIp.clear(); for (const client of this.scrcpyClients) { await client.controller!.close(); await client.close(); @@ -172,6 +178,60 @@ export class ScrcpyServer { } } }); + + // Per-device data sockets: all packets (config + data) for a device flow here in order. + // Clients open this after receiving a stream_available announcement on the control socket above. + this.wsServer.ws<{ streamId: string }>('/stream/:id', { + compression: uWS.DISABLED, + maxPayloadLength: 256 * 1024, + maxBackpressure: this.maxBackpressure, // 8 MB per stream + idleTimeout: 100, + sendPingsAutomatically: true, + + upgrade: (res, req, context) => { + const ip = req.getParameter(0); + if (!ip || !this.activeStreams.has(ip)) { + res.writeStatus('404 Not Found').end('Stream not found'); + return; + } + res.upgrade<{ streamId: string }>( + { streamId: ip }, + req.getHeader('sec-websocket-key'), + req.getHeader('sec-websocket-protocol'), + req.getHeader('sec-websocket-extensions'), + context + ); + }, + + open: (ws) => { + const { streamId } = ws.getUserData(); + if (!this.streamClients.has(streamId)) { + this.streamClients.set(streamId, new Set()); + } + this.streamClients.get(streamId)!.add(ws); + logger.debug(`[${streamId}] Device socket connected`); + + // Trigger a config reset so this client receives a fresh config + keyframe + const scrcpyClient = this.scrcpyClientsByIp.get(streamId); + if (scrcpyClient) { + this.resentConfigPackage(scrcpyClient) + .then(() => logger.debug(`[${streamId}] Config resent for new device socket client`)); + } + }, + + drain: (ws) => { + const { streamId } = ws.getUserData(); + logger + .getChild("Drain") + .info(`[${streamId}] Backpressure drained, streaming should be back to normal, buffered: ${ws.getBufferedAmount()}`); + }, + + close: (ws, code, message) => { + const { streamId } = ws.getUserData(); + this.streamClients.get(streamId)?.delete(ws); + logger.info(`[${streamId}] Device socket closed. Code: ${code}, Reason: ${Buffer.from(message).toString()}`); + }, + }); } async loadScrcpyServer() { @@ -189,10 +249,8 @@ export class ScrcpyServer { async startStreaming(adbConnection: Adb, deviceModel: string, flipWidth: boolean = false): Promise { const scrcpyOptions = new AdbScrcpyOptions3_3_3({ // scrcpy options - videoCodecOptions: new ScrcpyCodecOptions({ // Ensure Meta Quest compatibility - profile: H264Capabilities.maxProfile, - level: H264Capabilities.maxLevel, - }), + // No videoCodecOptions: let the Android encoder choose its own profile/level. + // Decoding is done by WebCodecs in the browser, which supports any H264/H265 profile. videoCodec: (useH265 ? "h265" : "h264"), // Video settings video: true, @@ -210,6 +268,8 @@ export class ScrcpyServer { logger.debug(`Starting scrcpy stream with ${useH265 ? "h265" : "h264"} codec`) try { + const streamIp = adbConnection.serial.split(':')[0]; // device IP, port stripped — stable across ADB reconnects + if (this.server == null) { await this.loadScrcpyServer(); } @@ -219,13 +279,11 @@ export class ScrcpyServer { logger.debug(`Sync adb with ${adbConnection.serial}`); const sync = await adbConnection.sync(); try { - const myself = this; - await sync.write({ filename: DefaultServerPath, file: new ReadableStream({ start: (controller) => { - controller.enqueue(new Uint8Array(myself.server)); + controller.enqueue(new Uint8Array(this.server)); controller.close(); }, }), @@ -258,7 +316,15 @@ export class ScrcpyServer { client.exited .then(() => { logger.info(`Scrcpy server exited for ${adbConnection.serial}`); - // Remove client from array + // Guard: only clean up the IP entry if THIS client is still the registered + // one. During a codec switch a new client may have already taken over the + // same IP, and we must not wipe its registration. + if (this.scrcpyClientsByIp.get(streamIp) === client) { + this.activeStreams.delete(streamIp); + this.scrcpyClientsByIp.delete(streamIp); + } + // Removing from the flat list is always safe (indexOf will simply return -1 + // if the list was already cleared during a codec switch). const index = this.scrcpyClients.indexOf(client); if (index > -1) { this.scrcpyClients.splice(index, 1); @@ -275,7 +341,10 @@ export class ScrcpyServer { logger.error(`Unexpected exit error for ${adbConnection.serial}: {error}`, { error }); } - // Remove client from array + if (this.scrcpyClientsByIp.get(streamIp) === client) { + this.activeStreams.delete(streamIp); + this.scrcpyClientsByIp.delete(streamIp); + } const index = this.scrcpyClients.indexOf(client); if (index > -1) { this.scrcpyClients.splice(index, 1); @@ -296,9 +365,13 @@ export class ScrcpyServer { ); } - // Store the controller of new client + // Register stream as active and store the controller logger.debug(`Saving new scrcpy client ${adbConnection.serial}`); + this.activeStreams.add(streamIp); + this.scrcpyClientsByIp.set(streamIp, client); this.scrcpyClients.push(client); + // Announce to all connected control clients so they open /stream/:streamIp + this.broadcastToClients(JSON.stringify({ type: "stream_available", streamId: streamIp })); // Print output of Scrcpy server client.output.pipeTo( @@ -324,25 +397,23 @@ export class ScrcpyServer { case "configuration": { // Handle configuration packet const newStreamConfig = JSON.stringify({ - streamId: adbConnection.serial, + streamId: streamIp, h265: useH265, type: "configuration", data: Buffer.from(packet.data).toString('base64'), // Convert Uint8Array to Base64 string }); - // Save packet for clients after this first packet emission - myself.broadcastToClients(newStreamConfig); + // Send to all clients on this device's data socket + myself.broadcastToStream(streamIp, newStreamConfig); logger.trace("Sending configuration frame {newStreamConfig}", { newStreamConfig }) - - // It is sent only once while opening the video stream and set the renderer - myself.scrcpyStreamConfig = newStreamConfig; } break; case "data": - // Handle data packet - myself.broadcastToClients( + // Handle data packet — sent on the dedicated per-device socket + myself.broadcastToStream( + streamIp, JSON.stringify({ - streamId: adbConnection.serial, + streamId: streamIp, h265: useH265, type: "data", keyframe: packet.keyframe, @@ -440,6 +511,32 @@ export class ScrcpyServer { return gotError; } + private safeSend(ws: uWS.WebSocket<{ streamId: string }>, packetJson: string): void { + const customLogger = logger.getChild("Drain"); + const { streamId } = ws.getUserData(); + + if (ws.getBufferedAmount() > 6 * 1024 * 1024) { // 6 MB threshold → 2 MB room before maxBackpressure + customLogger.warn(`[${streamId}] Dropping frame — client too slow`); + return; + } + + /* Possible returned values: + 0 : OK + 1 : Backpressure built up (but still queued) + 2 : Message dropped — backpressure limit exceeded + - The last one is the only interesting one + */ + if (ws.send(packetJson, false, true) === 2) { + customLogger.error(`[${streamId}] Video stream frame dropped...`); + } + } + + broadcastToStream(ip: string, packetJson: string): void { + this.streamClients.get(ip)?.forEach((client) => { + this.safeSend(client, packetJson); + }); + } + broadcastToClients(packetJson: string): void { const customLogger: Logger = logger.getChild("Drain"); this.wsClients.forEach((client) => { diff --git a/src/components/WebSocketManager/PlayerScreenCanvas.tsx b/src/components/WebSocketManager/PlayerScreenCanvas.tsx index 751a4e8..7ca5ba2 100644 --- a/src/components/WebSocketManager/PlayerScreenCanvas.tsx +++ b/src/components/WebSocketManager/PlayerScreenCanvas.tsx @@ -47,6 +47,7 @@ const PlayerScreenCanvas = ({ canvas, id, isPlaceholder, hideInfos, isLimitingWi if (showPopup) { if (popupref.current) { + popupref.current.querySelector('canvas')?.remove(); popupref.current.appendChild(canvas); canvas.classList.add("max-h-[95dvh]") canvas.classList.add("max-w-[95dvw]") @@ -63,8 +64,7 @@ const PlayerScreenCanvas = ({ canvas, id, isPlaceholder, hideInfos, isLimitingWi canvas.classList.add("max-w-[95dvw]") } - - + canvasref.current.querySelector('canvas')?.remove(); canvasref.current.appendChild(canvas); } } diff --git a/src/components/WebSocketManager/VideoStreamManager.tsx b/src/components/WebSocketManager/VideoStreamManager.tsx index b8573bd..0a1a560 100644 --- a/src/components/WebSocketManager/VideoStreamManager.tsx +++ b/src/components/WebSocketManager/VideoStreamManager.tsx @@ -89,18 +89,11 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V ReadableStreamDefaultController | undefined >(); const isDecoderHasConfig = new Map(); + // Tracks the codec each decoder was created with — used to detect mid-stream codec changes + const streamIsH265 = new Map(); - /** - * Creates a new ReadableStream for receiving and decoding H.264 video data associated with a specific device. - * - * This function initializes a ReadableStream that serves as the entry point for raw H.264 video data from a given device. - * It also sets up a TinyH264Decoder instance and pipes the ReadableStream's output to the decoder's writable stream. - * The decoded video frames are then rendered to an element referenced by `videoContainerRef`. - * - * @returns A ReadableStream that can be enqueued with data stream - */ async function newVideoStream(deviceId: string, useH265: boolean = false) { // Avoid having controller creation hell if connection is too fast @@ -154,8 +147,11 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V const decoder = new WebCodecsVideoDecoder({ codec: useH265 ? ScrcpyVideoCodecId.H265 : ScrcpyVideoCodecId.H264, renderer: renderer, + // Firefox on Linux has no hardware H264 WebCodecs path; "prefer-software" enables + // the software decoder (OpenH264) and avoids "encoding not supported" errors. + // H265 keeps "no-preference" so hardware acceleration is used when available. + hardwareAcceleration: useH265 ? "no-preference" : "prefer-software", }); - //TODO fix ce log logger.log("[Scrcpy-VideoStreamManager] Decoder for {useH265} ? \"h265\" : \"h264\", loaded", { useH265: "h265" }); // Create new ReadableStream used for scrcpy decoding const stream = new ReadableStream({ start(controller) { @@ -196,7 +192,86 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // ------------------------------------------------------------------------------------------------------------------- useEffect(() => { - // Open the WebSocket connection + let cleanedUp = false; + const deviceSockets = new Map(); + + // Opens a dedicated socket for a device at /stream/:deviceIp. + // All packets (config then data) arrive here in order — no split-channel ordering issues. + // Reconnects automatically after 1 s on unexpected close. + function connectDeviceSocket(streamId: string) { + if (cleanedUp) return; + + // Prevent the stale socket's onclose from firing a reconnect when we replace it + const existing = deviceSockets.get(streamId); + if (existing && existing.readyState < WebSocket.CLOSING) { + existing.onmessage = null; // prevent queued messages from old socket being processed after replacement + existing.onclose = null; + existing.close(); + // Reset decoder state: the stream is restarting, possibly with a different codec. + // Clearing these forces newVideoStream() to recreate the decoder on the next config packet. + readableControllers.delete(streamId); + isDecoderHasConfig.delete(streamId); + } + + const ws = new WebSocket(`ws://${host}:${port}/stream/${streamId}`); + deviceSockets.set(streamId, ws); + + ws.onmessage = (event) => { + // Deserialize the message and enqueue the data into the readable stream + const deserializedData = deserializeData(event.data); + + // Detect codec change on every configuration packet, regardless of channel ordering. + // The server can switch codec (h265↔h264) and restart streams; the new config packet + // may arrive on the old device socket before stream_available fires on the control + // socket, so we can't rely on connectDeviceSocket having run first. + if (deserializedData!.packet.type === "configuration") { + const knownCodec = streamIsH265.get(deserializedData!.streamId); + if (knownCodec !== undefined && knownCodec !== deserializedData!.useH265) { + readableControllers.delete(deserializedData!.streamId); + isDecoderHasConfig.delete(deserializedData!.streamId); + } + streamIsH265.set(deserializedData!.streamId, deserializedData!.useH265); + } + + // Create stream if new stream + if (!readableControllers.has(deserializedData!.streamId)) { + newVideoStream(deserializedData!.streamId, deserializedData!.useH265); + } + + const controller = readableControllers.get(deserializedData!.streamId); + + // Since we set very early the entry before the controller exists, + // this catch potential race conditions where controller do not exists + if (controller != undefined) { + // Enqueue data package to decoder stream + if (deserializedData!.packet) { + if ( + isDecoderHasConfig.get(deserializedData!.streamId) && + deserializedData!.packet.type == "data" + ) { + controller!.enqueue(deserializedData!.packet); + } else if ( + deserializedData!.packet.type == "configuration" + ) { + controller!.enqueue(deserializedData!.packet); + isDecoderHasConfig.set(deserializedData!.streamId, true); + } + } else { + logger.warn("[Scrcpy] Error piping to decoder writable stream, closing controller..."); + controller!.close(); + } + } + }; + + ws.onclose = () => { + if (!cleanedUp) { + logger.info(`[Scrcpy-VideoStreamManager] Device socket for ${streamId} closed, reconnecting in 1s...`); + setTimeout(() => connectDeviceSocket(streamId), 1000); + } + }; + } + + // Control socket: codec negotiation (client→server) + stream_available announcements (server→client) const socket = new WebSocket("ws://" + host + ":" + port); // Send browser's codecs compatibility socket.onopen = async () => { @@ -204,7 +279,7 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // Check if h264 is supported await VideoDecoder.isConfigSupported({ codec: "avc1.4D401E" }).then((r) => { supportH264 = r.supported!; - logger.info("[SCRCPY] Supports h264: {supportsH264}", { supportH264 }); + logger.info("[SCRCPY] Supports h264: {supportH264}", { supportH264 }); }) // Check if h265 is supported @@ -221,54 +296,32 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V socket.send(JSON.stringify({ "type": "codecVideo", - // @ts-expect-error + // @ts-expect-error "h264": supportH264, - // @ts-expect-error + // @ts-expect-error "h265": supportH265, - // @ts-expect-error + // @ts-expect-error "av1": supportAv1, })); } - // Handle incoming WebSocket messages + // Handle stream_available announcements — open a dedicated socket per device socket.onmessage = (event) => { - // Deserialize the message and enqueue the data into the readable stream - const deserializedData = deserializeData(event.data); - - // Create stream if new stream - if (!readableControllers.has(deserializedData!.streamId)) { - newVideoStream(deserializedData!.streamId, deserializedData!.useH265); - } - - const controller = readableControllers.get(deserializedData!.streamId); - - // Since we set very early the entry before the controller exists, - // this catch potential race conditions where controller do not exists - if (controller != undefined) { - // Enqueue data package to decoder stream - if (deserializedData!.packet) { - if ( - isDecoderHasConfig.get(deserializedData!.streamId) && - deserializedData!.packet.type == "data" - ) { - controller!.enqueue(deserializedData!.packet); - // Ensure starting stream with a configuration package holding keyframe - } else if ( - //\!isDecoderHasConfig.get(deserializedData!.streamId) && - deserializedData!.packet.type == "configuration" - ) { - controller!.enqueue(deserializedData!.packet); - isDecoderHasConfig.set(deserializedData!.streamId, true); - } - } else { - logger.warn("[Scrcpy] Error piping to decoder writable stream, closing controller..."); - controller!.close(); - } + const data = JSON.parse(event.data); + if (data.type === "stream_available") { + logger.info(`[Scrcpy-VideoStreamManager] Stream available: ${data.streamId}`); + connectDeviceSocket(data.streamId); } }; socket.onclose = () => { - logger.info("[Scrcpy-VideoStreamManager] Closing readable"); + logger.info("[Scrcpy-VideoStreamManager] Control socket closed"); + }; + + return () => { + cleanedUp = true; + socket.close(); + deviceSockets.forEach(ws => ws.close()); }; }, []); diff --git a/vite.config.ts b/vite.config.ts index 359673f..7548930 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,8 +23,7 @@ export default defineConfig(({ mode }) => { preview: serverConfig, server: serverConfig, optimizeDeps: { - exclude: ["@yume-chan/adb-scrcpy", "@yume-chan/stream-extra", "@yume-chan/scrcpy-decoder-tinyh264"], - include: ['@yume-chan/scrcpy-decoder-tinyh264 > yuv-buffer', '@yume-chan/scrcpy-decoder-tinyh264 > yuv-canvas'] + exclude: ["@yume-chan/adb-scrcpy", "@yume-chan/stream-extra"], }, define: { 'process.env.MONITOR_WS_PORT': JSON.stringify(env.MONITOR_WS_PORT),