diff --git a/src/modules b/src/modules index 0d4e35e6a..d0f0a842c 160000 --- a/src/modules +++ b/src/modules @@ -1 +1 @@ -Subproject commit 0d4e35e6a1748a52f3c29610b4fe84e1ba0ffcac +Subproject commit d0f0a842c874f110c1d93ff6b6f0a17a98b09838 diff --git a/src/plugin.ts b/src/plugin.ts index 717e0fa4f..16e884a4d 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -408,7 +408,7 @@ class DevicePlugin extends BasePlugin { if (device.platform.toLowerCase() === 'ios' && !isRemoteOrCloudSession) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const { server, port, scheme, sessionId } = Object.values(driver.sessions)[0].wda.jwproxy; + const { server, port, scheme } = driver.sessions[sessionId].wda.jwproxy; const proxiedInfo = { proxyUrl: `${scheme}://${server}:${port}`, @@ -577,7 +577,12 @@ class DevicePlugin extends BasePlugin { await unblockDeviceMatchingFilter({ session_id: sessionId }); log.info(`📱 Unblocking the device that is blocked for session ${sessionId}`); const res = await next(); - await registerAndroidWebSocketHandlers(DevicePlugin.httpServer, DevicePlugin.adbInstance); + try { + await waitForWebsocketToBeDeregister(this.pluginArgs, DevicePlugin.httpServer); + await DevicePlugin.registerWebSocket(this.pluginArgs); + } catch (err) { + log.info('Socket server not removed within 5000ms. So not registering again'); + } return res; } } diff --git a/web/package-lock.json b/web/package-lock.json index e6473aa4a..76809e720 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -18,7 +18,9 @@ "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-json-view": "^1.21.3", - "react-router-dom": "^6.21.1" + "react-router-dom": "^6.21.1", + "react-use-websocket": "^3.0.0", + "reconnecting-websocket": "^4.4.0" }, "devDependencies": { "@types/react": "^18.2.43", @@ -3696,6 +3698,20 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-websocket": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz", + "integrity": "sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==", + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/reconnecting-websocket": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", + "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==" + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -6732,6 +6748,17 @@ "prop-types": "^15.6.2" } }, + "react-use-websocket": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz", + "integrity": "sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==", + "requires": {} + }, + "reconnecting-websocket": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", + "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==" + }, "regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", diff --git a/web/package.json b/web/package.json index d3123cacc..36ba2f089 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,8 @@ "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-json-view": "^1.21.3", - "react-router-dom": "^6.21.1" + "react-router-dom": "^6.21.1", + "react-use-websocket": "^3.0.0" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/web/src/assets/device-loading.gif b/web/src/assets/device-loading.gif new file mode 100644 index 000000000..edf7a36af Binary files /dev/null and b/web/src/assets/device-loading.gif differ diff --git a/web/src/components/streaming/AndroidStream.tsx b/web/src/components/streaming/AndroidStream.tsx index c99d438b9..db5dbb475 100644 --- a/web/src/components/streaming/AndroidStream.tsx +++ b/web/src/components/streaming/AndroidStream.tsx @@ -6,12 +6,14 @@ import DeviceFarmApiService from '../../api-service'; import HomeIcon from '@mui/icons-material/Home'; import { Camera, Upload, Close } from '@mui/icons-material'; import { toolBarControl } from './util.ts'; +import DeviceLoading from '../../assets/device-loading.gif'; +import useWebSocket from 'react-use-websocket'; const MAX_HEIGHT = 720; const MAX_WIDTH = 720; function AndroidStream() { - const [imageSrc, setImageSrc] = useState(''); + const [imageSrc, setImageSrc] = useState(DeviceLoading); const [file, setFile] = useState(null); const containerElement = useRef(null); @@ -19,24 +21,12 @@ function AndroidStream() { const canvasElement = useRef(null); let interactionHandler: SimpleInterationHandler | null; - // eslint-disable-next-line prefer-const - let [ws, setWebSocket] = useState(undefined); const handleWebSocketMessage = (event: { data: any }) => { const blob = event.data; const url = URL.createObjectURL(blob); setImageSrc(url); }; - const createWebSocketConnection = (wsUrl: string) => { - ws = new WebSocket(wsUrl); - ws.addEventListener('message', handleWebSocketMessage); - ws.addEventListener('close', () => { - console.log('WebSocket connection closed. Reconnecting...'); - createWebSocketConnection(wsUrl); - }); - return ws; - }; - const getParamsFromUrl = () => { if (window.location.hash.includes('?')) { const params = new URLSearchParams(window.location.hash.split('?')[1]); @@ -51,22 +41,15 @@ function AndroidStream() { return { port: 8004, host: '127.0.0.1', udid: '' }; } }; - useEffect(() => { - const { host, udid, port } = getParamsFromUrl() as any; - const wsUrl = `ws://${host}:${port}/android-stream/${udid}`; - - createWebSocketConnection(wsUrl); - setWebSocket(ws); - return () => { - // Clean up the WebSocket connection when the component is unmounted - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - ws.removeEventListener('message', handleWebSocketMessage); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - ws.close(); - }; - }, []); + const { host, udid, port } = getParamsFromUrl() as any; + const wsUrl = `ws://${host}:${port}/android-stream/${udid}`; + const { sendMessage } = useWebSocket(wsUrl, { + share: false, + shouldReconnect: () => true, + onMessage: handleWebSocketMessage, + reconnectInterval: 1500, + reconnectAttempts: 15, + }); useEffect(() => { const { width, height } = getParamsFromUrl() as any; @@ -82,14 +65,14 @@ function AndroidStream() { videoElement.current as any, canvasElement.current, containerElement.current, - ws, + { send: sendMessage }, { width, height }, ); } }, []); async function onToolbarControlClick(controlAction: string) { - await toolBarControl(ws, controlAction, getParamsFromUrl); + await toolBarControl({ send: sendMessage } as any, controlAction, getParamsFromUrl); } const handleFileChange = (event: React.ChangeEvent) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -185,9 +168,7 @@ function AndroidStream() { icon: (
- +
), - name: '' + name: '', }, { action: 'close', diff --git a/web/src/components/streaming/ios-stream.tsx b/web/src/components/streaming/ios-stream.tsx index 7cf109ced..2eb2d77f4 100644 --- a/web/src/components/streaming/ios-stream.tsx +++ b/web/src/components/streaming/ios-stream.tsx @@ -5,12 +5,38 @@ import { StreamingToolBar } from './toolbar'; import { SimpleInterationHandler } from '../../libs/simple-interation-handler'; import { Camera, Close, Upload } from '@mui/icons-material'; import { toolBarControl, uploadFile } from './util.ts'; +import DeviceLoading from '../../assets/device-loading.gif'; +import useWebSocket from 'react-use-websocket'; const MAX_HEIGHT = 720; const MAX_WIDTH = 720; function IOSStream() { + const getParamsFromUrl = () => { + if (window.location.hash.includes('?')) { + const params = new URLSearchParams(window.location.hash.split('?')[1]); + return { + port: params.get('port'), + udid: params.get('udid'), + host: params.get('host'), + width: params.get('width'), + height: params.get('height'), + streamPort: params.get('streamPort'), + }; + } else { + return { port: 8004, host: '127.0.0.1', udid: '' }; + } + }; + // const [imageSrc, setImageSrc] = useState(''); + const { host, port, udid, width, height, streamPort } = getParamsFromUrl() as any; + const wsUrl = `ws://${host}:${port}/ios-stream/${udid}`; + const { sendMessage } = useWebSocket(wsUrl, { + share: false, + shouldReconnect: () => true, + reconnectInterval: 1500, + reconnectAttempts: 15, + }); const containerElement = useRef(null); const videoElement = useRef(null); @@ -19,22 +45,10 @@ function IOSStream() { let interactionHandler: SimpleInterationHandler | null; // eslint-disable-next-line prefer-const - let [ws, setWebSocket] = useState(undefined); - - const createWebSocketConnection = (wsUrl: string) => { - ws = new WebSocket(wsUrl); - // ws.addEventListener('message', handleWebSocketMessage); - ws.addEventListener('close', () => { - console.log('WebSocket connection closed. Reconnecting...'); - createWebSocketConnection(wsUrl); - }); - setWebSocket(ws); - return ws; - }; const uploadAUT = async () => { - await uploadFile(file, getParamsFromUrl) - } + await uploadFile(file, getParamsFromUrl); + }; const handleFileChange = (event: React.ChangeEvent) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -45,36 +59,7 @@ function IOSStream() { setFile(selectedFile); }; - const getParamsFromUrl = () => { - if (window.location.hash.includes('?')) { - const params = new URLSearchParams(window.location.hash.split('?')[1]); - return { - port: params.get('port'), - udid: params.get('udid'), - host: params.get('host'), - width: params.get('width'), - height: params.get('height'), - streamPort: params.get('streamPort'), - }; - } else { - return { port: 8004, host: '127.0.0.1', udid: '' }; - } - }; - useEffect(() => { - const { host, port, udid } = getParamsFromUrl() as any; - const wsUrl = `ws://${host}:${port}/ios-stream/${udid}`; - - createWebSocketConnection(wsUrl); - return () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - ws.close(); - }; - }, []); - useEffect(() => { - const { width, height } = getParamsFromUrl() as any; - if ( !interactionHandler && canvasElement.current && @@ -86,14 +71,20 @@ function IOSStream() { videoElement.current as any, canvasElement.current, containerElement.current, - ws, + { send: sendMessage } as any, { width, height }, ); } }, []); - const { host, port, streamPort } = getParamsFromUrl() as any; + async function onToolbarControlClick(controlAction: string) { - await toolBarControl(ws, controlAction, getParamsFromUrl); + await toolBarControl( + { + send: sendMessage, + } as any, + controlAction, + getParamsFromUrl, + ); } return (
@@ -108,10 +99,14 @@ function IOSStream() { > - +
), - name: '' + name: '', }, { action: 'close', diff --git a/web/src/components/streaming/streaming.css b/web/src/components/streaming/streaming.css index 5bd8f3a78..79f4a58c2 100644 --- a/web/src/components/streaming/streaming.css +++ b/web/src/components/streaming/streaming.css @@ -9,7 +9,7 @@ .device-container { display: flex; flex-direction: row; - gap: 0; + gap: 10px; } .toolbar-container { diff --git a/web/yarn.lock b/web/yarn.lock index 0dc6dcc17..c7ac8a353 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1778,7 +1778,7 @@ react-chartjs-2@^5.2.0: resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz" integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== -"react-dom@^17.0.0 || ^16.3.0 || ^15.5.4", "react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0: +"react-dom@^17.0.0 || ^16.3.0 || ^15.5.4", "react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, "react-dom@>= 16.8.0", react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -1855,13 +1855,23 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -"react@^15.0.2 || ^16.0.0 || ^17.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^16.3.0 || ^15.5.4", "react@^17.0.0 || ^18.0.0", react@^18.2.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0: +react-use-websocket@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz" + integrity sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw== + +"react@^15.0.2 || ^16.0.0 || ^17.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^16.3.0 || ^15.5.4", "react@^17.0.0 || ^18.0.0", react@^18.2.0, "react@>= 16.8.0", react@>=16.6.0, react@>=16.8, react@>=16.8.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz"