Skip to content
This repository was archived by the owner on Jul 10, 2024. It is now read-only.

Commit 5bc7bd6

Browse files
feat:stream live video of test execution (#41)
1 parent 00ab1cb commit 5bc7bd6

File tree

19 files changed

+249
-38
lines changed

19 files changed

+249
-38
lines changed

.github/labeler.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
version: 1
22
labels:
33
- label: "enhancement"
4-
titles:
4+
title:
55
- "^feat:.*"
66
- label: "bug"
7-
titles:
7+
title:
88
- "^fix:.*"
99
- label: "test"
10-
titles:
10+
title:
1111
- "^test:.*"
1212
- label: "skip-changelog"
13-
titles:
13+
title:
1414
- "^test:.*"

.github/release-drafter-template.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ name-template: "Release v$RESOLVED_VERSION 🔥"
22
tag-template: "v$RESOLVED_VERSION"
33
categories:
44
- title: "🚀 Features"
5-
label:
5+
labels:
66
- "enhancement"
77
- title: "🐛 Bug Fixes"
8-
label:
8+
labels:
99
- "bug"
1010

1111
# Only include the following labels in the release notes. All other labels are ignored.

migrations/001_create_table.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var createSessionTable = function (queryInterface, Sequelize) {
4747
automation_name: { type: Sequelize.TEXT, allowNull: false },
4848
device_name: { type: Sequelize.TEXT, allowNull: false },
4949
platform_version: { type: Sequelize.TEXT, allowNull: false },
50+
live_stream_port: { type: Sequelize.INTEGER, allowNull: true },
5051
app: { type: Sequelize.TEXT, allowNull: true },
5152
browser_name: { type: Sequelize.TEXT, allowNull: true },
5253
udid: { type: Sequelize.TEXT, allowNull: false },

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@
4545
"cors": "^2.8.5",
4646
"debug": "^4.3.2",
4747
"express": "^4.17.1",
48+
"get-port": "^5.1.1",
4849
"http-status": "^1.5.0",
4950
"lodash": "^4.17.21",
5051
"lokijs": "^1.5.12",
52+
"mjpeg-proxy": "^0.3.0",
5153
"reflect-metadata": "^0.1.13",
5254
"sequelize": "^6.6.5",
5355
"sequelize-cli": "^6.2.0",

src/app/controllers/session-controller.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import fs from "fs";
66
import { CommandLogs, HttpLogs, Logs, Profiling } from "../../models";
77
import * as path from "path";
88
import { parseSessionFilterParams } from "../utils/common-utils";
9+
import { MjpegProxy } from "mjpeg-proxy";
910

1011
export class SessionController extends BaseController {
12+
private static mjpegProxyCache: Map<number, any> = new Map();
13+
1114
public initializeRoutes(router: Router, config: any) {
1215
router.get("/", this.getSessions.bind(this));
1316
router.get("/:sessionId", this.getSessionBySessionId.bind(this));
@@ -19,6 +22,7 @@ export class SessionController extends BaseController {
1922
router.get("/:sessionId/logs/debug", this.getDebugLogs.bind(this));
2023
router.get("/:sessionId/profiling_data", this.getProfilingData.bind(this));
2124
router.get("/:sessionId/http_logs", this.getHttpLogs.bind(this));
25+
router.get("/:sessionId/live_video", this.getVideo.bind(this));
2226
}
2327

2428
public async getSessions(request: Request, response: Response, next: NextFunction) {
@@ -161,4 +165,23 @@ export class SessionController extends BaseController {
161165
});
162166
this.sendSuccessResponse(response, logs);
163167
}
168+
169+
public async getVideo(request: Request, response: Response, next: NextFunction) {
170+
let sessionId: string = request.params.sessionId;
171+
let session = await Session.findOne({
172+
where: {
173+
session_id: sessionId,
174+
},
175+
});
176+
const proxyPort = session?.live_stream_port;
177+
if (!proxyPort) {
178+
return this.sendFailureResponse(response, { message: "Live stream not available" });
179+
}
180+
181+
if (!SessionController.mjpegProxyCache.has(proxyPort)) {
182+
const url = `${request.protocol}://${request.hostname}:${proxyPort}`;
183+
SessionController.mjpegProxyCache.set(proxyPort, new MjpegProxy(url));
184+
}
185+
SessionController.mjpegProxyCache.get(proxyPort)?.proxyRequest(request, response);
186+
}
164187
}

src/models/session.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ class Session extends Model<Session> {
8888
})
8989
browser_name!: string;
9090

91+
@AllowNull(true)
92+
@Column({
93+
type: DataTypes.INTEGER,
94+
})
95+
live_stream_port!: number;
96+
9197
@AllowNull(false)
9298
@Column({
9399
type: DataTypes.STRING,

src/plugin/http-logger/ios-http-logger.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { retryInterval } from "asyncbox";
55
import _ from "lodash";
66
import { pluginLogger } from "../../loggers/plugin-logger";
77
import { logger } from "../../loggers/logger";
8+
89
export type IosNetworkProfilerOptions = {
910
udid: string;
1011
platformVersion: string;
@@ -72,26 +73,26 @@ class IosNetworkProfiler implements IHttpLogger {
7273
}
7374

7475
getLogs(): Array<any> {
75-
return Object.keys(this.logs);
76+
return [];
7677
}
7778

7879
private async initializeListeners() {
79-
this.remoteDebugger.addClientEventListener("Network.requestWillBeSent", (err: any, event: any) => {
80+
this.remoteDebugger.addClientEventListener("NetworkEvent", (err: any, event: any) => {
8081
pluginLogger.info(event);
8182
this.logs[event.requestId] = event;
8283
});
8384
this.remoteDebugger.addClientEventListener("Network.responseReceived", async (err: any, event: any) => {
85+
pluginLogger.info(event);
8486
this.logs[event.requestId] = Object.assign({}, this.logs[event.requestId], {
8587
response: event,
8688
});
8789
});
8890
const page = _.find(await this.remoteDebugger.selectApp("http://0.0.0.0:4723/welcome"), (page) => {
89-
logger.info(page);
9091
return page.url == "http://0.0.0.0:4723/welcome";
9192
});
9293
const [appIdKey, pageIdKey] = page.id.split(".").map((id: string) => parseInt(id, 10));
9394
pluginLogger.info(`AppId: ${appIdKey} and pageIdKey: ${pageIdKey}`);
94-
await this.remoteDebugger.selectPage(appIdKey, pageIdKey);
95+
//await this.remoteDebugger.selectPage(appIdKey, pageIdKey);
9596
}
9697

9798
private static async waitForRemoteDebugger(remoteDebugger: RemoteDebugger) {

src/plugin/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PluginCliArgs } from "../interfaces/PluginCliArgs";
99
import * as express from "express";
1010
import { registerDebugMiddlware } from "./debugger";
1111
import _ from "lodash";
12+
import getPort from "get-port";
1213

1314
const sessionMap: Map<string, SessionManager> = new Map();
1415
const IGNORED_COMMANDS = ["getScreenshot", "stopRecordingScreen", "startRecordingScreen"];
@@ -100,6 +101,10 @@ class AppiumDashboardPlugin extends BasePlugin {
100101
"appium:nativeWebScreenshot": true, //to make screenshot endpoint work in android webview tests,
101102
};
102103

104+
if (rawCapabilities?.["platformName"].toLowerCase() == "android" && !rawCapabilities?.["appium:mjpegServerPort"]) {
105+
newCapabilities["appium:mjpegServerPort"] = await getPort();
106+
}
107+
103108
Object.keys(newCapabilities).forEach((k) => {
104109
args[2][k] = newCapabilities[k];
105110
args[2].firstMatch[0][k] = newCapabilities[k];

src/plugin/session-manager.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
isAppProfilingSupported,
1111
isHttpLogsSuppoted,
1212
isAndroidSession,
13+
getMjpegServerPort,
1314
} from "./utils/plugin-utils";
1415
import {
1516
getLogs,
@@ -204,6 +205,7 @@ class SessionManager {
204205
driver: command.driver,
205206
});
206207
}
208+
207209
let { desired } = this.sessionInfo.capabilities;
208210
let buildName = desired["dashboard:build"];
209211
let projectName = desired["dashboard:project"];
@@ -220,6 +222,8 @@ class SessionManager {
220222
build = await getOrCreateNewBuild({ buildName, projectId: project?.id });
221223
}
222224

225+
await this.initializeScreenShotFolder();
226+
await this.startScreenRecording(command.driver);
223227
await Session.create({
224228
...this.sessionInfo,
225229
start_time: new Date(),
@@ -228,11 +232,10 @@ class SessionManager {
228232
device_info,
229233
is_profiling_available,
230234
name: name || null,
235+
live_stream_port: await getMjpegServerPort(command.driver, this.sessionInfo.session_id),
231236
} as any);
232237

233238
await this.saveCommandLog(command, null);
234-
await this.initializeScreenShotFolder();
235-
return await this.startScreenRecording(command.driver);
236239
}
237240

238241
public async sessionTerminated(options: { sessionTimedOut: boolean } = { sessionTimedOut: false }) {

0 commit comments

Comments
 (0)