Skip to content

Commit

Permalink
added ios real device streaming initial logic (#1066)
Browse files Browse the repository at this point in the history
  • Loading branch information
saikrishna321 authored Mar 27, 2024
1 parent 17fb8d8 commit 6fb0c56
Show file tree
Hide file tree
Showing 14 changed files with 1,393 additions and 2,191 deletions.
3,357 changes: 1,288 additions & 2,069 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"@types/node-persist": "^3.1.5",
"appium-adb": "^11.0.3",
"appium-chromedriver": "^5.6.19",
"appium-ios-device": "^2.7.6",
"appium-ios-device": "^2.7.13",
"appium-xcuitest-driver": "^7.6.1",
"async-lock": "^1.2.8",
"async-wait-until": "^2.0.12",
"asyncbox": "^3.0.0",
Expand All @@ -103,6 +104,7 @@
"jsonschema": "1.4.1",
"lodash": "4.17.21",
"lokijs": "^1.5.12",
"mjpeg-consumer": "^2.0.0",
"mjpeg-proxy": "^0.3.0",
"node-abort-controller": "^3.1.1",
"node-cache": "^5.1.2",
Expand Down Expand Up @@ -154,8 +156,7 @@
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@wdio/types": "^8.24.2",
"appium-uiautomator2-driver": "^2.42.1",
"appium-xcuitest-driver": "^5.14.0",
"appium-uiautomator2-driver": "^3.0.4",
"babel-eslint": "10.1.0",
"chai": "4.3.10",
"chai-as-promised": "^7.1.1",
Expand Down Expand Up @@ -268,6 +269,9 @@
},
"liveStreaming": {
"type": "boolean"
},
"wdaBundleId": {
"type": "string"
}
},
"title": "Appium device farm plugin",
Expand Down
5 changes: 3 additions & 2 deletions src/app/routers/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { DeviceFarmManager } from '../../device-managers';
import Container from 'typedi';
import { IPluginArgs } from '../../interfaces/IPluginArgs';
import { IDevice } from '../../interfaces/IDevice';
import { installStreamingApp, pressHome } from '../../modules/androidStreaming';
import { createDriverSession, installAndroidStreamingApp } from '../../helpers';
import { installAndroidStreamingApp } from '../../helpers';
import { createDriverSession, installIOSAppOnRealDevice } from '../../modules/DeviceHelper';

const SERVER_UP_TIME = new Date().toISOString();

Expand Down Expand Up @@ -245,6 +245,7 @@ function register(router: Router, pluginArgs: IPluginArgs) {
router.get('/node/:host/status', _.curry(nodeAdbStatusOnOtherHost)(pluginArgs.bindHostOrIp));

router.post('/installAndroidStreamingApp', installAndroidStreamingApp);
router.post('/installiOSWDA', installIOSAppOnRealDevice);
router.post('/appiumSession', createDriverSession);
//router.post('/tap', clickElementFromScreen);
// node status
Expand Down
1 change: 1 addition & 0 deletions src/device-managers/IOSDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export default class IOSDeviceManager implements IDeviceManager {
deviceType: 'real',
platform: this.getDevicePlatformName(name),
host,
wdaBundleId: pluginArgs.wdaBundleId,
totalUtilizationTimeMilliSec: totalUtilizationTimeMilliSec,
sessionStartTime: 0,
derivedDataPath: this.prepareDerivedDataPath(pluginArgs.derivedDataPath, udid, true),
Expand Down
69 changes: 1 addition & 68 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { FakeModuleLoader } from './fake-module-loader';
import { IExternalModuleLoader } from './interfaces/IExternalModule';
import fs from 'fs';
import { downloadFile } from './modules/downloadApk';
import Adb from '@devicefarmer/adbkit';
import { sleep } from 'asyncbox';
import { DEVICE_CONNECTIONS_FACTORY } from 'appium-xcuitest-driver/build/lib/device-connections-factory';
import { Request, Response } from 'express';
import {
allowRecordingPermissions,
Expand All @@ -28,8 +27,6 @@ import {
} from './modules/androidStreaming';
import { ADB } from 'appium-adb';
import { getDevice } from './data-service/device-service';
import http from 'http';
import https from 'https';
import { W3CNewSessionResponse } from './interfaces/ISessionCapability';

const APPIUM_VENDOR_PREFIX = 'appium:';
Expand Down Expand Up @@ -279,70 +276,6 @@ export function getSessionIdFromUrl(url: string) {
}
return null;
}

export async function createDriverSession(request: Request, res: Response) {
const udid = request.body.udid;
const systemPort = request.body.systemPort;
const capabilitiesToCreateSession = {
capabilities: {
alwaysMatch: {
platformName: 'android',
'appium:automationName': 'UIAutomator2',
'appium:newCommandTimeout': 120,
'appium:waitForIdleTimeout': 10,
'appium:udid': udid,
'appium:systemPort': systemPort,
},
firstMatch: [{}],
},
desiredCapabilities: {
platformName: 'android',
'appium:automationName': 'UIAutomator2',
'appium:newCommandTimeout': 120,
'appium:waitForIdleTimeout': 10,
'appium:udid': udid,
'appium:systemPort': systemPort,
},
};

const device: any = await getDevice({ udid: [udid] });
const config: any = {
method: 'post',
url: `${device.host}/wd/hub/session`,
timeout: 60000,
headers: {
'Content-Type': 'application/json',
},
data: capabilitiesToCreateSession,
};
console.log(config);
let sessionDetails: W3CNewSessionResponse | null = null;
let errorMessage: string | null = null;
try {
const response = await axios(config);
log.debug('Response from browser create session', JSON.stringify(response.data));

// Appium endpoint returns session details w3c format: https://github.com/jlipps/simple-wd-spec?tab=readme-ov-file#new-session
sessionDetails = response.data as unknown as W3CNewSessionResponse;
// check if we have error in response by checking sessionDetails.value type
if ('error' in sessionDetails.value) {
log.error(`Error while creating session: ${sessionDetails.value.error}`);
errorMessage = sessionDetails.value.error as string;
return res.status(400).send(errorMessage);
}
return res.status(200).send({ status: 200, sessionID: sessionDetails.value.sessionId });
} catch (error: AxiosError<any> | any) {
log.debug(`Received error from remote node: ${JSON.stringify(error)}`);
if (error instanceof AxiosError) {
errorMessage = JSON.stringify(error.response?.data);
return res.status(400).send(errorMessage);
} else {
errorMessage = error;
return res.status(400).send(errorMessage);
}
}
}

export async function downloadAndroidStreamAPK() {
const destinationPath = path.join(__dirname, 'stream.apk');
if (!fs.existsSync(destinationPath)) {
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export interface IDevice {
width?: string;
height?: string;
liveStreaming?: boolean;
wdaBundleId?: string;
}
2 changes: 2 additions & 0 deletions src/interfaces/IPluginArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface IPluginArgs {
bindHostOrIp: string;
enableDashboard: boolean;
liveStreaming: boolean;
wdaBundleId: string;


// development purposes
Expand Down Expand Up @@ -103,4 +104,5 @@ export const DefaultPluginArgs: IPluginArgs = {
removeDevicesFromDatabaseBeforeRunningThePlugin: false,
remoteConnectionTimeout: 60000,
liveStreaming: false,
wdaBundleId: '',
};
2 changes: 1 addition & 1 deletion src/modules
11 changes: 9 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ import {
stripAppiumPrefixes,
hasCloudArgument,
loadExternalModules,
downloadAndroidStreamAPK, streamAndroid
downloadAndroidStreamAPK,
streamAndroid,
} from './helpers';
import { addProxyHandler, registerProxyMiddlware } from './proxy/wd-command-proxy';
import ChromeDriverManager from './device-managers/ChromeDriverManager';
Expand Down Expand Up @@ -80,6 +81,7 @@ const DEVICE_MANAGER_LOCK_NAME = 'DeviceManager';
let platform: any;
let androidDeviceType: any;
let iosDeviceType: any;
let wdaBundleId: string;
let hasEmulators: any;
let proxy: any;
let externalModule: any;
Expand Down Expand Up @@ -179,6 +181,7 @@ class DevicePlugin extends BasePlugin {
platform = pluginArgs.platform;
androidDeviceType = pluginArgs.androidDeviceType;
iosDeviceType = pluginArgs.iosDeviceType;
wdaBundleId = pluginArgs.wdaBundleId;
if (pluginArgs.proxy !== undefined) {
log.info(`Adding proxy for axios: ${JSON.stringify(pluginArgs.proxy)}`);
proxy = pluginArgs.proxy;
Expand Down Expand Up @@ -341,7 +344,11 @@ class DevicePlugin extends BasePlugin {
debugLog(`📱${pendingSessionId} --- Forwarded session response: ${JSON.stringify(session)}`);
} else {
log.debug('📱 Creating session on the same node');
if (this.pluginArgs.liveStreaming && !this.pluginArgs.cloud) {
if (
this.pluginArgs.platform.toLowerCase() === 'android' &&
this.pluginArgs.liveStreaming &&
!this.pluginArgs.cloud
) {
log.info('📱 Live streaming argument is set to true, preparing device for live streaming');
const destination = await downloadAndroidStreamAPK();
const adbClient = await ADB.createADB({});
Expand Down
4 changes: 3 additions & 1 deletion web/src/api-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default class DeviceFarmApiService {
public static createSession(udid: string, systemPort: number) {
return apiClient.makePOSTRequest('/appiumSession', {}, {udid, systemPort});
}

public static installWDAOnDevice(udid: string) {
return apiClient.makePOSTRequest('/installiOSWDA', {}, { udid });
}
public static getPendingSessionsCount() {
return apiClient.makeGETRequest('/queue/length', {});
}
Expand Down
86 changes: 41 additions & 45 deletions web/src/components/device-card/device-card/device-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,59 +81,55 @@ export default class DeviceCard extends React.Component<IDeviceCardProps, any> {
const handleLiveStreamClick = async () => {
this.setState({ isLoading: true }); // Set loading state to true when the button is clicked

const { udid, systemPort } = this.props.device;

try {
console.log('Live Stream');
if(!this.props.device.session_id) {
console.log('Session ID is not available, creating a session');
const sessionCreationResponse = await DeviceFarmApiService.createSession(udid, systemPort);
if (sessionCreationResponse.status === 200) {
await this.blockDevice(udid, host);
console.log('Session created successfully');
const { udid, systemPort, platform } = this.props.device;
console.log('Platform:', platform);
if (platform === 'android') {
try {
console.log('Live Stream');
if(!this.props.device.session_id) {
console.log('Session ID is not available, creating a session');
const sessionCreationResponse = await DeviceFarmApiService.createSession(udid, systemPort);
if (sessionCreationResponse.status === 200) {
await this.blockDevice(udid, host);
console.log('Session created successfully');
} else {
console.error('Error creating session:', sessionCreationResponse);
}
}
if (!this.props.device.liveStreaming) {
console.log('Live Streaming property is false, starting a ws session');
const response = await DeviceFarmApiService.androidStreamingAppInstalled(udid, systemPort);
console.log('Response:', response);
if (response.status === 200) {
window.location.href = `#/androidStream?port=${appiumPort}&host=${appiumHost}&udid=${udid}&width=${response.device.width}&height=${response.device.height}`;
}
} else {
console.error('Error creating session:', sessionCreationResponse);
window.location.href = `#/androidStream?port=${appiumPort}&host=${appiumHost}&udid=${udid}&width=${this.props.device.width}&height=${this.props.device.height}`;
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred while trying to stream the device');
} finally {
this.setState({ isLoading: false }); // Set loading state back to false when the request is complete
}
if (!this.props.device.liveStreaming) {
console.log('Live Streaming property is false, starting a ws session');
const response = await DeviceFarmApiService.androidStreamingAppInstalled(udid, systemPort);
console.log('Response:', response);
if (response.status === 200) {
window.location.href = `#/androidStream?port=${appiumPort}&host=${appiumHost}&udid=${udid}&width=${response.device.width}&height=${response.device.height}`;
} else {
if (!this.props.device.session_id) {
const wdaInstallResponse = await DeviceFarmApiService.installWDAOnDevice(udid);
if (wdaInstallResponse.status === 200) {
const sessionCreationResponse = await DeviceFarmApiService.createSession(udid, systemPort);
if (sessionCreationResponse.status === 200) {
window.location.href = `#/iOSStream?port=${this.props.device.mjpegServerPort}&host=${appiumHost}`;
//window.location.href = `#/androidStream?port=${appiumPort}&host=${appiumHost}&udid=${udid}&width=${response.device.width}&height=${response.device.height}`;
}
} else {
alert(`Error creating session check logs ${wdaInstallResponse}`);
}
this.setState({ isLoading: false });
} else {
window.location.href = `#/androidStream?port=${appiumPort}&host=${appiumHost}&udid=${udid}&width=${this.props.device.width}&height=${this.props.device.height}`;
window.location.href = `#/iOSStream?port=${this.props.device.mjpegServerPort}&host=${appiumHost}`;
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred while trying to stream the device');
} finally {
this.setState({ isLoading: false }); // Set loading state back to false when the request is complete
}
};

// const liveStreaming = () => {
// return (
// <div style={{ paddingLeft: '2px' }}>
// <button
// className="device-info-card__body_stream-device"
// onClick={async () => {
// console.log('Live Stream');
// const response = await DeviceFarmApiService.androidStreamingAppInstalled(udid, systemPort);
// if(response.status === 200) {
// (window.location.href = `#/androidStream?port=${appiumPort}&host=${appiumHost}&udid=${this.props.device.udid}`)
// } else {
// alert('Please install the app to stream the device');
// }
// }
// }
// >
// Live Stream
// </button>
// </div>
// );
// };
const blockButton = () => {
if (busy) {
return;
Expand Down
33 changes: 33 additions & 0 deletions web/src/components/streaming/IOSStream.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

function IOSStream() {

const getParamsFromUrl = () => {
if (window.location.hash.includes('?')) {
const params = new URLSearchParams(window.location.hash.split('?')[1]);
return {
port: params.get('port'),
host: params.get('host'),
};
} else {
return { port: 8004, host: '127.0.0.1', udid: '' };
}
};
const params = getParamsFromUrl(); // Call the function to get parameters
return (
<div>
<div>
<img
style={{
maxHeight: 730 + 'px',
maxWidth: 730 + 'px',
width: 'auto',
position: 'absolute',
}}
src={`http://${params.host}:${params.port}`}
/>
</div>
</div>
);
}

export default IOSStream;
1 change: 1 addition & 0 deletions web/src/interfaces/IDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface IDevice {
liveStreaming: boolean;
width: string;
height: string;
mjpegServerPort: number;
}
2 changes: 2 additions & 0 deletions web/src/router/RootRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import DeviceFarm from '../pages/DeviceFarm';
import Builds from '../pages/Builds';
import Session from '../pages/Session';
import AndroidStream from '../components/streaming/AndroidStream';
import IOSStream from '../components/streaming/IOSStream.tsx';
function RootRouter() {
return (
<Routes>
<Route path="/" element={<DeviceFarm />} />
<Route path="/builds" element={<Builds />} />
<Route path="/builds/:buildId/session/:sessionId" element={<Session />} />
<Route path="/androidStream" element={<AndroidStream />} />
<Route path="/iOSStream" element={<IOSStream />} />
</Routes>
);
}
Expand Down

0 comments on commit 6fb0c56

Please sign in to comment.