@xenon-device-management/xenon 1.9.2 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.9.2",
3
+ "version": "1.10.1",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
@@ -12,7 +12,7 @@
12
12
  "postinstall": "node scripts/generate-prisma.js",
13
13
  "build": "node scripts/generate-prisma.js && tsc -b && npm run build:copy",
14
14
  "build:all": "npm run build:xenon && npm run build",
15
- "build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/",
15
+ "build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/ && mkdir -p lib/src/device-managers/android/vendor && cp src/device-managers/android/vendor/*.jar lib/src/device-managers/android/vendor/",
16
16
  "build:schema": "node scripts/generate-types-from-schema.js",
17
17
  "build:xenon": "sh scripts/build-xenon.sh",
18
18
  "dev": "npm run db:migrate && npm run build && npm run plugin:reinstall && npm run server",
package/lib/schema.json CHANGED
@@ -475,9 +475,16 @@
475
475
  "description": "Live-streaming options.",
476
476
  "properties": {
477
477
  "androidH264": {
478
- "type": "boolean",
479
478
  "default": false,
480
- "description": "Use scrcpy H.264 for Android live preview (WebCodecs in the browser) instead of the MJPEG screencap loop. Falls back to MJPEG when unsupported."
479
+ "description": "Android live preview: false/omitted = MJPEG; true = H.264 via scrcpy; { \"source\": \"scrcpy\" | \"screenrecord\" } to pick the capture source explicitly. Falls back to MJPEG when unsupported.",
480
+ "oneOf": [
481
+ { "type": "boolean" },
482
+ {
483
+ "type": "object",
484
+ "additionalProperties": false,
485
+ "properties": { "source": { "type": "string", "enum": ["scrcpy", "screenrecord"] } }
486
+ }
487
+ ]
481
488
  }
482
489
  }
483
490
  }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAndroidH264 = resolveAndroidH264;
4
+ /**
5
+ * Normalize the accepted flag shapes to one `{ enabled, source }`. Config values
6
+ * are untrusted and can arrive as a real boolean, a STRINGIFIED boolean
7
+ * (`"true"` / `"false"` — some Appium config sources and the Xenon Control form
8
+ * serialize booleans as strings), or a `{ source }` object:
9
+ *
10
+ * false | "false" | undefined | "" → disabled (pure MJPEG)
11
+ * true | "true" → enabled, scrcpy (default source)
12
+ * { source } → enabled, that source (scrcpy default)
13
+ * any other truthy scalar → enabled, scrcpy
14
+ *
15
+ * The string handling matters: without it a stringified `"false"` is truthy and
16
+ * would wrongly ENABLE the feature, so the flag could never be turned off from a
17
+ * source that stringifies booleans.
18
+ */
19
+ function resolveAndroidH264(cfg) {
20
+ if (!cfg || cfg === 'false')
21
+ return { enabled: false, source: 'scrcpy' };
22
+ if (cfg === true || cfg === 'true')
23
+ return { enabled: true, source: 'scrcpy' };
24
+ if (typeof cfg === 'object') {
25
+ const source = cfg.source;
26
+ return { enabled: true, source: source === 'screenrecord' ? 'screenrecord' : 'scrcpy' };
27
+ }
28
+ return { enabled: true, source: 'scrcpy' };
29
+ }
@@ -64,6 +64,7 @@ const InspectorService_1 = require("../../services/InspectorService");
64
64
  const StreamTicketService_1 = require("../../services/token/StreamTicketService");
65
65
  const PluginContext_1 = require("../../PluginContext");
66
66
  const streamType_1 = require("./streamType");
67
+ const androidH264Config_1 = require("./androidH264Config");
67
68
  const recording_store_1 = require("../../services/recording/recording-store");
68
69
  const scopeGuard_1 = require("../../middleware/scopeGuard");
69
70
  const roleGuard_1 = require("../../middleware/roleGuard");
@@ -562,7 +563,8 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
562
563
  });
563
564
  }
564
565
  try {
565
- const flagOn = !!((_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs.streaming) === null || _a === void 0 ? void 0 : _a.androidH264);
566
+ const h264Cfg = (0, androidH264Config_1.resolveAndroidH264)((_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs.streaming) === null || _a === void 0 ? void 0 : _a.androidH264);
567
+ const flagOn = h264Cfg.enabled;
566
568
  // A recording device keeps MJPEG preview so we never run screenrecord and
567
569
  // the recording's screencap pipeline against the same device at once.
568
570
  const recording = yield typedi_1.Container.get(recording_store_1.RecordingStore).isRecording(udid);
@@ -574,7 +576,7 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
574
576
  else if (streamType === 'h264') {
575
577
  // H.264 preview: start the scrcpy/screenrecord service. Do NOT also start
576
578
  // the MJPEG screencap loop — one capture pipeline per device.
577
- yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid);
579
+ yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
578
580
  }
579
581
  else {
580
582
  mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
@@ -708,7 +710,7 @@ router.get('/:udid/stream/status', (req, res) => __awaiter(void 0, void 0, void
708
710
  return res.status(404).send('Device not found');
709
711
  // Advertise which transport the frontend player should use. A recording
710
712
  // device keeps MJPEG (one capture pipeline per device).
711
- const flagOn = !!((_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs.streaming) === null || _a === void 0 ? void 0 : _a.androidH264);
713
+ const flagOn = (0, androidH264Config_1.resolveAndroidH264)((_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs.streaming) === null || _a === void 0 ? void 0 : _a.androidH264).enabled;
712
714
  const recording = yield typedi_1.Container.get(recording_store_1.RecordingStore).isRecording(udid);
713
715
  const type = (0, streamType_1.resolveStreamType)(device.platform, flagOn, recording);
714
716
  const h264Path = type === 'h264' ? `/xenon/api/control/${encodeURIComponent(udid)}/stream/h264` : undefined;
@@ -60,6 +60,8 @@ const logger_1 = __importDefault(require("../../logger"));
60
60
  const device_store_1 = require("../../data-service/device-store");
61
61
  const H264Multiplexer_1 = require("./H264Multiplexer");
62
62
  const h264NalParser_1 = require("./h264NalParser");
63
+ const androidH264Config_1 = require("../../app/routers/androidH264Config");
64
+ const PluginContext_1 = require("../../PluginContext");
63
65
  const IDLE_TIMEOUT_MS = 600000; // stop a stream after 10 min with zero viewers
64
66
  const RESTART_MIN_INTERVAL_MS = 300; // fast restart after a healthy stream hits the cap/rotation
65
67
  // After this many consecutive restarts that produced no frames (device gone,
@@ -81,6 +83,10 @@ let AndroidH264StreamService = class AndroidH264StreamService {
81
83
  constructor() {
82
84
  this.sessions = new Map();
83
85
  this.startPromises = new Map();
86
+ // udids where scrcpy proved incompatible (e.g. it aborts on init — the Exynos
87
+ // "stack corruption" crash). Once seen, skip scrcpy for that device and use the
88
+ // screenrecord H.264 source directly instead of re-crashing scrcpy every start.
89
+ this.scrcpyIncompatible = new Set();
84
90
  this.startWatchdog();
85
91
  }
86
92
  startWatchdog() {
@@ -106,7 +112,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
106
112
  var _a;
107
113
  return (_a = this.sessions.get(udid)) === null || _a === void 0 ? void 0 : _a.mux;
108
114
  }
109
- start(udid) {
115
+ start(udid, opts) {
110
116
  return __awaiter(this, void 0, void 0, function* () {
111
117
  const inflight = this.startPromises.get(udid);
112
118
  if (inflight)
@@ -115,6 +121,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
115
121
  if (existing && existing.status === 'running')
116
122
  return existing.mux;
117
123
  const promise = (() => __awaiter(this, void 0, void 0, function* () {
124
+ var _a, _b;
118
125
  const mux = new H264Multiplexer_1.H264Multiplexer();
119
126
  const session = { status: 'running', mux };
120
127
  this.sessions.set(udid, session);
@@ -129,7 +136,13 @@ let AndroidH264StreamService = class AndroidH264StreamService {
129
136
  resolveConfig();
130
137
  }
131
138
  };
132
- session.capture = yield this.openCapture(udid, onPacket);
139
+ // Resolve the capture source: an explicit opts.source wins; otherwise
140
+ // read the server-wide flag config. Resolving here (not just at the REST
141
+ // call site) means the WS auto-start path — start(udid) with no opts —
142
+ // also honours a configured { source: 'screenrecord' } instead of
143
+ // silently defaulting to scrcpy.
144
+ const source = (_a = opts === null || opts === void 0 ? void 0 : opts.source) !== null && _a !== void 0 ? _a : (0, androidH264Config_1.resolveAndroidH264)((_b = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs.streaming) === null || _b === void 0 ? void 0 : _b.androidH264).source;
145
+ session.capture = yield this.openCapture(udid, onPacket, source);
133
146
  // Give the first keyframe/config a moment so callers get a ready stream,
134
147
  // but never block start-up indefinitely.
135
148
  yield Promise.race([firstConfig, new Promise((r) => setTimeout(r, 3000))]);
@@ -169,13 +182,112 @@ let AndroidH264StreamService = class AndroidH264StreamService {
169
182
  logger_1.default.info(`[${udid}] H.264 stream terminated.`);
170
183
  });
171
184
  }
185
+ /**
186
+ * Select the capture source: `screenrecord` (legacy, rollback path) or
187
+ * `scrcpy` (default). Dispatches to the matching producer.
188
+ */
189
+ openCapture(udid, onPacket, source) {
190
+ return __awaiter(this, void 0, void 0, function* () {
191
+ return source === 'screenrecord'
192
+ ? this.openScreenrecordCapture(udid, onPacket)
193
+ : this.openScrcpyCapture(udid, onPacket);
194
+ });
195
+ }
196
+ /**
197
+ * scrcpy-server capture: pushes/launches the vendored server jar over adb
198
+ * and streams its Annex-B H.264 socket output through a fresh
199
+ * {@link H264NalParser}. Unlike screenrecord, scrcpy has no time cap, so a
200
+ * 'close' while the session is still 'running' is unexpected — treat it as
201
+ * a crash and stop the session rather than silently going dark.
202
+ */
203
+ openScrcpyCapture(udid, onPacket) {
204
+ return __awaiter(this, void 0, void 0, function* () {
205
+ var _a;
206
+ // A device where scrcpy already proved incompatible skips straight to the
207
+ // screenrecord H.264 source — no point re-crashing scrcpy every time.
208
+ if (this.scrcpyIncompatible.has(udid)) {
209
+ logger_1.default.info(`[${udid}] scrcpy known-incompatible here; using screenrecord H.264 source.`);
210
+ return this.openScreenrecordCapture(udid, onPacket);
211
+ }
212
+ try {
213
+ return yield this.startScrcpyOrThrow(udid, onPacket);
214
+ }
215
+ catch (e) {
216
+ // scrcpy failed to produce frames (aborted on init, closed early, or timed
217
+ // out). Fall back to the screenrecord H.264 source — still real H.264
218
+ // (~30fps), far better than dropping all the way to ~1fps MJPEG.
219
+ this.scrcpyIncompatible.add(udid);
220
+ logger_1.default.warn(`[${udid}] scrcpy H.264 unavailable (${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}); falling back to screenrecord H.264 source.`);
221
+ return this.openScreenrecordCapture(udid, onPacket);
222
+ }
223
+ });
224
+ }
225
+ /**
226
+ * Start scrcpy and resolve only once it produces its FIRST packet (proof it
227
+ * works). Reject if it closes/errors/times-out before that — the signal a
228
+ * device is scrcpy-incompatible (e.g. the Exynos `stack corruption` abort),
229
+ * which {@link openScrcpyCapture} turns into a screenrecord fallback.
230
+ */
231
+ startScrcpyOrThrow(udid, onPacket) {
232
+ return __awaiter(this, void 0, void 0, function* () {
233
+ const { ScrcpyServerSession, scrcpyMaxSizeFromDims } = yield Promise.resolve().then(() => __importStar(require('./ScrcpyServerSession')));
234
+ const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
235
+ const maxSize = scrcpyMaxSizeFromDims(Number(device === null || device === void 0 ? void 0 : device.screenWidth), Number(device === null || device === void 0 ? void 0 : device.screenHeight));
236
+ const parser = new h264NalParser_1.H264NalParser();
237
+ const session = new ScrcpyServerSession(udid);
238
+ return new Promise((resolve, reject) => {
239
+ let settled = false;
240
+ const fail = (e) => {
241
+ if (settled)
242
+ return;
243
+ settled = true;
244
+ clearTimeout(timer);
245
+ try {
246
+ session.stop();
247
+ }
248
+ catch (_a) {
249
+ /* best-effort */
250
+ }
251
+ reject(e instanceof Error ? e : new Error(String(e)));
252
+ };
253
+ // scrcpy forces an initial keyframe, so a healthy device produces its first
254
+ // packet within ~1–2s; this bound only catches a hung (never-frames) scrcpy.
255
+ const timer = setTimeout(() => fail(new Error('scrcpy produced no frames within 8s')), 8000);
256
+ session.on('data', (b) => {
257
+ for (const p of parser.push(b)) {
258
+ onPacket(p);
259
+ if (!settled) {
260
+ settled = true;
261
+ clearTimeout(timer);
262
+ resolve({ kill: () => session.stop() });
263
+ }
264
+ }
265
+ });
266
+ session.on('close', () => {
267
+ var _a;
268
+ if (!settled) {
269
+ fail(new Error('scrcpy closed before producing frames'));
270
+ return;
271
+ }
272
+ // Crash after it was streaming: stop the session. On the client's
273
+ // reconnect this device is now marked incompatible, so it gets screenrecord.
274
+ if (((_a = this.sessions.get(udid)) === null || _a === void 0 ? void 0 : _a.status) === 'running') {
275
+ logger_1.default.warn(`[${udid}] scrcpy stream closed unexpectedly; stopping H.264 session.`);
276
+ this.scrcpyIncompatible.add(udid);
277
+ this.stop(udid);
278
+ }
279
+ });
280
+ session.start(maxSize).catch(fail);
281
+ });
282
+ });
283
+ }
172
284
  /**
173
285
  * Spawn screenrecord with the resolved adb binary (never a bare `adb` — the
174
286
  * GUI-launch PATH trap; see 1.8.2) and feed its Annex-B output through a
175
287
  * fresh {@link H264NalParser}. Restarts on process exit (cap/rotation) while
176
288
  * the session is still running.
177
289
  */
178
- openCapture(udid, onPacket) {
290
+ openScreenrecordCapture(udid, onPacket) {
179
291
  return __awaiter(this, void 0, void 0, function* () {
180
292
  var _a;
181
293
  const { default: AndroidDeviceManager } = yield Promise.resolve().then(() => __importStar(require('../AndroidDeviceManager')));
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.ScrcpyServerSession = exports.SCRCPY_DEVICE_JAR_PATH = void 0;
49
+ exports.buildScrcpyServerArgs = buildScrcpyServerArgs;
50
+ exports.scrcpyMaxSizeFromDims = scrcpyMaxSizeFromDims;
51
+ exports.parseAdbForwardPort = parseAdbForwardPort;
52
+ const child_process_1 = require("child_process");
53
+ const events_1 = require("events");
54
+ const net_1 = __importDefault(require("net"));
55
+ const util_1 = require("util");
56
+ const typedi_1 = require("typedi");
57
+ const logger_1 = __importDefault(require("../../logger"));
58
+ const scrcpyVersion_1 = require("./scrcpyVersion");
59
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
60
+ const LOCAL_ABSTRACT = 'scrcpy'; // device-side socket name scrcpy listens on
61
+ exports.SCRCPY_DEVICE_JAR_PATH = '/data/local/tmp/scrcpy-server-manual.jar';
62
+ /**
63
+ * The argv passed to the resolved adb AFTER any `-s <udid>` — a headless,
64
+ * video-only scrcpy-server launch. All three metadata channels are disabled
65
+ * (`send_device_meta=false`, `send_codec_meta=false`, `send_frame_meta=false`)
66
+ * so the socket carries plain Annex-B H.264 that the existing H264NalParser
67
+ * consumes unchanged. `send_dummy_byte=true` (the tunnel_forward readiness byte)
68
+ * is explicit because the socket reader skips exactly one leading byte.
69
+ * Arg NAMES verified against the vendored scrcpy 2.7 jar's dex; the version
70
+ * constant and this argv move together (see scrcpyVersion.ts / vendor/README.md).
71
+ * No `scid` → the server listens on `localabstract:scrcpy` (per-device namespace).
72
+ */
73
+ function buildScrcpyServerArgs(opts) {
74
+ return [
75
+ 'shell',
76
+ `CLASSPATH=${opts.jarDevicePath}`,
77
+ 'app_process',
78
+ '/',
79
+ 'com.genymobile.scrcpy.Server',
80
+ opts.version,
81
+ 'tunnel_forward=true',
82
+ 'audio=false',
83
+ 'control=false',
84
+ 'video=true',
85
+ 'video_codec=h264',
86
+ `max_size=${opts.maxSize}`,
87
+ 'video_bit_rate=4000000',
88
+ 'max_fps=30',
89
+ 'send_device_meta=false',
90
+ 'send_codec_meta=false',
91
+ 'send_frame_meta=false',
92
+ 'send_dummy_byte=true',
93
+ 'cleanup=true',
94
+ ];
95
+ }
96
+ /**
97
+ * scrcpy `max_size` caps the device's LONGER edge (single int, aspect preserved).
98
+ * Derive it so the SHORTER edge lands near `targetShortEdge` (matches today's
99
+ * ~720-wide screenrecord downscale). Never upscales.
100
+ */
101
+ function scrcpyMaxSizeFromDims(sw, sh, targetShortEdge = 720) {
102
+ const shortE = Math.min(sw, sh);
103
+ const longE = Math.max(sw, sh);
104
+ if (!Number.isFinite(shortE) || !Number.isFinite(longE) || shortE <= 0 || longE <= 0) {
105
+ return targetShortEdge * 2; // safe default longer-edge cap
106
+ }
107
+ if (shortE <= targetShortEdge)
108
+ return longE; // no upscale
109
+ return Math.round(longE * (targetShortEdge / shortE));
110
+ }
111
+ /** Parse the local TCP port that `adb forward tcp:0 …` prints on stdout. */
112
+ function parseAdbForwardPort(stdout) {
113
+ const port = parseInt(String(stdout).trim(), 10);
114
+ if (!Number.isInteger(port) || port <= 0) {
115
+ throw new Error(`Unparseable adb forward port: ${JSON.stringify(stdout)}`);
116
+ }
117
+ return port;
118
+ }
119
+ class ScrcpyServerSession extends events_1.EventEmitter {
120
+ constructor(udid) {
121
+ super();
122
+ this.udid = udid;
123
+ this.stopped = false;
124
+ this.closeEmitted = false;
125
+ this.adbPath = 'adb';
126
+ this.hostArgs = [];
127
+ this.base = []; // [...hostArgs, '-s', udid]
128
+ }
129
+ /** Guarantees at most one public 'close' event, and none once the session was intentionally stopped. */
130
+ emitClosedOnce() {
131
+ if (this.closeEmitted || this.stopped)
132
+ return;
133
+ this.closeEmitted = true;
134
+ this.emit('close');
135
+ }
136
+ start(maxSize) {
137
+ return __awaiter(this, void 0, void 0, function* () {
138
+ var _a, _b, _c;
139
+ const { default: AndroidDeviceManager } = yield Promise.resolve().then(() => __importStar(require('../AndroidDeviceManager')));
140
+ const adb = yield typedi_1.Container.get(AndroidDeviceManager).getAdbForDevice(this.udid);
141
+ if (this.stopped)
142
+ throw new Error('session stopped during start');
143
+ if (!((_a = adb === null || adb === void 0 ? void 0 : adb.executable) === null || _a === void 0 ? void 0 : _a.path))
144
+ throw new Error(`[${this.udid}] adb executable path not resolved`);
145
+ this.adbPath = adb.executable.path;
146
+ this.hostArgs = (adb === null || adb === void 0 ? void 0 : adb.adbHost) && (adb === null || adb === void 0 ? void 0 : adb.adbPort) ? ['-H', adb.adbHost, '-P', String(adb.adbPort)] : [];
147
+ this.base = [...this.hostArgs, '-s', this.udid];
148
+ try {
149
+ // 1) push jar (idempotent enough; overwrite is cheap and safe). Push
150
+ // destination and the CLASSPATH must be the same path — use the one
151
+ // constant so editing it can never point CLASSPATH at an unpushed jar.
152
+ yield execFileAsync(this.adbPath, [...this.base, 'push', (0, scrcpyVersion_1.scrcpyServerJarPath)(), exports.SCRCPY_DEVICE_JAR_PATH]);
153
+ if (this.stopped)
154
+ throw new Error('session stopped during start');
155
+ // 2) app_process (long-lived). buildScrcpyServerArgs + SCRCPY_DEVICE_JAR_PATH
156
+ // are defined in THIS file (Task 2) — reference them directly (no self-import).
157
+ const serverArgs = buildScrcpyServerArgs({
158
+ version: scrcpyVersion_1.SCRCPY_SERVER_VERSION,
159
+ jarDevicePath: exports.SCRCPY_DEVICE_JAR_PATH,
160
+ maxSize,
161
+ });
162
+ this.proc = (0, child_process_1.spawn)(this.adbPath, [...this.base, ...serverArgs]);
163
+ (_b = this.proc.stdout) === null || _b === void 0 ? void 0 : _b.resume(); // scrcpy-server writes video to the socket, not stdout; drain so it can't backpressure
164
+ (_c = this.proc.stderr) === null || _c === void 0 ? void 0 : _c.on('data', (d) => logger_1.default.debug(`[${this.udid}] scrcpy-server: ${d.toString().trim()}`));
165
+ this.proc.on('close', () => { this.emitClosedOnce(); });
166
+ this.proc.on('error', (e) => {
167
+ // A ChildProcess emitting 'error' with zero listeners crashes the process — always have one.
168
+ logger_1.default.warn(`[${this.udid}] scrcpy-server proc error: ${e.message}`);
169
+ this.emitClosedOnce();
170
+ });
171
+ // 3) forward a local port to the device socket, then connect
172
+ // (parseAdbForwardPort is defined in this file — Task 3 — so call it directly)
173
+ const { stdout } = yield execFileAsync(this.adbPath, [...this.base, 'forward', 'tcp:0', `localabstract:${LOCAL_ABSTRACT}`]);
174
+ const port = parseAdbForwardPort(stdout);
175
+ this.forwardSpec = `tcp:${port}`;
176
+ if (this.stopped)
177
+ throw new Error('session stopped during start');
178
+ yield this.connectWithRetry(port);
179
+ if (this.stopped)
180
+ throw new Error('stopped during start');
181
+ }
182
+ catch (e) {
183
+ this.cleanup();
184
+ throw e;
185
+ }
186
+ });
187
+ }
188
+ connectWithRetry(port, attempt = 0) {
189
+ return new Promise((resolve, reject) => {
190
+ if (this.stopped) {
191
+ reject(new Error('stopped'));
192
+ return;
193
+ }
194
+ const sock = net_1.default.connect(port, '127.0.0.1');
195
+ let settled = false;
196
+ // `adb forward` accepts the LOCAL tcp connection immediately (~5ms) and only
197
+ // THEN dials the device-side scrcpy socket; if scrcpy-server hasn't bound it
198
+ // yet, adb closes us with zero bytes. So 'connect' is NOT proof the server is
199
+ // ready — a connect that closes/errors before any byte means "not ready yet"
200
+ // and we retry. A connection counts as real only once scrcpy's readiness
201
+ // dummy byte (send_dummy_byte=true) arrives. This is why the dummy byte exists.
202
+ const retry = (reason) => {
203
+ if (settled)
204
+ return;
205
+ settled = true;
206
+ sock.removeListener('data', onFirstData);
207
+ sock.removeListener('error', retryErr);
208
+ sock.removeListener('close', retryClose);
209
+ sock.destroy(); // don't leak the failed socket
210
+ if (attempt < 19 && !this.stopped) { // 20 attempts total (0..19)
211
+ setTimeout(() => this.connectWithRetry(port, attempt + 1).then(resolve, reject), 100);
212
+ }
213
+ else {
214
+ reject(new Error(`scrcpy connect failed after ${attempt + 1} attempts: ${reason}`));
215
+ }
216
+ };
217
+ const retryErr = (e) => retry(e.message || 'socket error');
218
+ const retryClose = () => retry('closed before data');
219
+ const onFirstData = (first) => {
220
+ if (settled)
221
+ return;
222
+ settled = true;
223
+ sock.removeListener('error', retryErr);
224
+ sock.removeListener('close', retryClose);
225
+ if (this.stopped) {
226
+ sock.destroy();
227
+ reject(new Error('stopped during connect'));
228
+ return;
229
+ }
230
+ this.socket = sock;
231
+ // The first byte is scrcpy's readiness dummy byte; the raw H.264 Annex-B
232
+ // stream (SPS/PPS/IDR…) begins at byte 1 of this same chunk.
233
+ const rest = first.subarray(1);
234
+ sock.on('data', (chunk) => { if (!this.stopped)
235
+ this.emit('data', chunk); });
236
+ sock.on('error', (e) => logger_1.default.warn(`[${this.udid}] scrcpy socket error: ${e.message}`)); // steady-state: log; 'close' follows
237
+ sock.on('close', () => { this.emitClosedOnce(); });
238
+ if (rest.length && !this.stopped)
239
+ this.emit('data', rest);
240
+ resolve();
241
+ };
242
+ sock.once('error', retryErr);
243
+ sock.once('close', retryClose);
244
+ sock.once('data', onFirstData);
245
+ });
246
+ }
247
+ cleanup() {
248
+ var _a, _b;
249
+ this.stopped = true; // must be first: gates emitClosedOnce + connectWithRetry + connect-handler against a proc/socket event racing this cleanup
250
+ try {
251
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.destroy();
252
+ }
253
+ catch ( /* best-effort */_c) { /* best-effort */ }
254
+ try {
255
+ (_b = this.proc) === null || _b === void 0 ? void 0 : _b.kill('SIGKILL');
256
+ }
257
+ catch ( /* best-effort */_d) { /* best-effort */ }
258
+ if (this.forwardSpec) {
259
+ const spec = this.forwardSpec;
260
+ this.forwardSpec = undefined; // clear first so a repeated stop()/cleanup() doesn't re-spawn adb
261
+ (0, child_process_1.execFile)(this.adbPath, [...this.base, 'forward', '--remove', spec], () => undefined);
262
+ }
263
+ }
264
+ stop() {
265
+ this.stopped = true;
266
+ this.cleanup();
267
+ }
268
+ }
269
+ exports.ScrcpyServerSession = ScrcpyServerSession;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SCRCPY_SERVER_VERSION = void 0;
7
+ exports.scrcpyServerJarFilename = scrcpyServerJarFilename;
8
+ exports.scrcpyServerJarPath = scrcpyServerJarPath;
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * Pinned scrcpy-server version. The vendored jar filename AND the `app_process`
12
+ * <version> argument both derive from this — they must always agree, so this is
13
+ * the single source of truth. Bump procedure: see vendor/README.md.
14
+ */
15
+ exports.SCRCPY_SERVER_VERSION = '2.7';
16
+ function scrcpyServerJarFilename() {
17
+ return `scrcpy-server-${exports.SCRCPY_SERVER_VERSION}.jar`;
18
+ }
19
+ /** Absolute path to the vendored jar, valid in src (ts-node) and lib (built). */
20
+ function scrcpyServerJarPath() {
21
+ return path_1.default.join(__dirname, 'vendor', scrcpyServerJarFilename());
22
+ }
@@ -114,6 +114,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
114
114
  if (httpServer) {
115
115
  (0, h264StreamWs_1.attachH264Ws)(httpServer, {
116
116
  redeem: (ticket, udid) => typedi_1.Container.get(StreamTicketService_1.StreamTicketService).redeem(ticket, udid),
117
+ // No source opt on purpose: start() resolves the capture source from the
118
+ // streaming.androidH264 config itself, so this WS auto-start honours a
119
+ // { source: 'screenrecord' } rollback just like the REST stream/start.
117
120
  startStream: (udid) => typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid),
118
121
  });
119
122
  }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const androidH264Config_1 = require("../../src/app/routers/androidH264Config");
5
+ describe('resolveAndroidH264', () => {
6
+ it('undefined/false → disabled', () => {
7
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)(undefined)).to.deep.equal({ enabled: false, source: 'scrcpy' });
8
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)(false)).to.deep.equal({ enabled: false, source: 'scrcpy' });
9
+ });
10
+ it('true → enabled with scrcpy (the new meaning of true)', () => {
11
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)(true)).to.deep.equal({ enabled: true, source: 'scrcpy' });
12
+ });
13
+ it('object → enabled; source defaults to scrcpy, honored when explicit', () => {
14
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)({})).to.deep.equal({ enabled: true, source: 'scrcpy' });
15
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)({ source: 'screenrecord' })).to.deep.equal({
16
+ enabled: true,
17
+ source: 'screenrecord',
18
+ });
19
+ });
20
+ it('object with an unrecognized source → defaults to scrcpy', () => {
21
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)({ source: 'bogus' })).to.deep.equal({
22
+ enabled: true,
23
+ source: 'scrcpy',
24
+ });
25
+ });
26
+ it('stringified booleans are handled (form/config may serialize as strings)', () => {
27
+ // "true" enables; "false" MUST disable (without this, a truthy "false" would
28
+ // wrongly enable and the flag could never be turned off).
29
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)('true')).to.deep.equal({ enabled: true, source: 'scrcpy' });
30
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)('false')).to.deep.equal({ enabled: false, source: 'scrcpy' });
31
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)('')).to.deep.equal({ enabled: false, source: 'scrcpy' });
32
+ });
33
+ it('null and other truthy scalars are tolerated', () => {
34
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)(null)).to.deep.equal({ enabled: false, source: 'scrcpy' });
35
+ (0, chai_1.expect)((0, androidH264Config_1.resolveAndroidH264)('yes')).to.deep.equal({ enabled: true, source: 'scrcpy' });
36
+ });
37
+ });
@@ -14,16 +14,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
+ const typedi_1 = require("typedi");
17
18
  const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
19
+ const PluginContext_1 = require("../../src/PluginContext");
18
20
  // Lifecycle/state only — the capture seam (openCapture) is stubbed so the test
19
21
  // drives packet flow without a real device or screenrecord process.
20
22
  function make() {
21
23
  const svc = Object.create(AndroidH264StreamService_1.default.prototype);
22
24
  svc.sessions = new Map();
23
25
  svc.startPromises = new Map();
26
+ svc.scrcpyIncompatible = new Set();
24
27
  return svc;
25
28
  }
26
29
  describe('AndroidH264StreamService', () => {
30
+ // start(udid) with no opts reads the flag config from PluginContext. Seed a
31
+ // known default before each test (streaming undefined → source 'scrcpy'); the
32
+ // WS-source tests override it in-test. Set (not remove) so TypeDI never hits a
33
+ // ServiceNotFoundError on the next Container.get.
34
+ beforeEach(() => {
35
+ typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
36
+ });
27
37
  it('start() creates a multiplexer, seeds it with the config packet, and resolves', () => __awaiter(void 0, void 0, void 0, function* () {
28
38
  const svc = make();
29
39
  svc.openCapture = (_udid, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
@@ -80,4 +90,99 @@ describe('AndroidH264StreamService', () => {
80
90
  (0, chai_1.expect)(killed).to.equal(true);
81
91
  (0, chai_1.expect)(svc.getMultiplexer('udid-z')).to.equal(undefined);
82
92
  }));
93
+ it('start({source:"scrcpy"}) uses the scrcpy capture seam', () => __awaiter(void 0, void 0, void 0, function* () {
94
+ const svc = make();
95
+ let used = '';
96
+ svc.openScrcpyCapture = (_u, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
97
+ used = 'scrcpy';
98
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
99
+ return { kill: () => undefined };
100
+ });
101
+ svc.openScreenrecordCapture = () => __awaiter(void 0, void 0, void 0, function* () { used = 'screenrecord'; return { kill: () => undefined }; });
102
+ yield svc.start('udid-s', { source: 'scrcpy' });
103
+ (0, chai_1.expect)(used).to.equal('scrcpy');
104
+ }));
105
+ it('start({source:"screenrecord"}) uses the legacy capture seam', () => __awaiter(void 0, void 0, void 0, function* () {
106
+ const svc = make();
107
+ let used = '';
108
+ svc.openScrcpyCapture = () => __awaiter(void 0, void 0, void 0, function* () { used = 'scrcpy'; return { kill: () => undefined }; });
109
+ svc.openScreenrecordCapture = (_u, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
110
+ used = 'screenrecord';
111
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
112
+ return { kill: () => undefined };
113
+ });
114
+ yield svc.start('udid-r', { source: 'screenrecord' });
115
+ (0, chai_1.expect)(used).to.equal('screenrecord');
116
+ }));
117
+ // The WebSocket auto-start path calls start(udid) with NO opts. It must still
118
+ // honour the configured capture source (read from PluginContext), not silently
119
+ // default to scrcpy — otherwise a { source: 'screenrecord' } rollback is bypassed.
120
+ it('start(udid) with no opts uses the source configured in PluginContext (screenrecord)', () => __awaiter(void 0, void 0, void 0, function* () {
121
+ const svc = make();
122
+ typedi_1.Container.set(PluginContext_1.PluginContext, {
123
+ pluginArgs: { streaming: { androidH264: { source: 'screenrecord' } } },
124
+ });
125
+ let used = '';
126
+ svc.openScrcpyCapture = () => __awaiter(void 0, void 0, void 0, function* () {
127
+ used = 'scrcpy';
128
+ return { kill: () => undefined };
129
+ });
130
+ svc.openScreenrecordCapture = (_u, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
131
+ used = 'screenrecord';
132
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
133
+ return { kill: () => undefined };
134
+ });
135
+ yield svc.start('udid-ws-sr'); // no opts — mimics the WS startStream(udid) path
136
+ (0, chai_1.expect)(used).to.equal('screenrecord');
137
+ }));
138
+ it('start(udid) with no opts uses the source configured in PluginContext (scrcpy)', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ const svc = make();
140
+ typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: { streaming: { androidH264: true } } });
141
+ let used = '';
142
+ svc.openScrcpyCapture = (_u, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
143
+ used = 'scrcpy';
144
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
145
+ return { kill: () => undefined };
146
+ });
147
+ svc.openScreenrecordCapture = () => __awaiter(void 0, void 0, void 0, function* () {
148
+ used = 'screenrecord';
149
+ return { kill: () => undefined };
150
+ });
151
+ yield svc.start('udid-ws-scrcpy'); // no opts
152
+ (0, chai_1.expect)(used).to.equal('scrcpy');
153
+ }));
154
+ it('openScrcpyCapture uses scrcpy when it produces frames (startScrcpyOrThrow resolves)', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ const svc = make();
156
+ let usedScreenrecord = false;
157
+ svc.startScrcpyOrThrow = () => __awaiter(void 0, void 0, void 0, function* () { return ({ kill: () => undefined }); });
158
+ svc.openScreenrecordCapture = () => __awaiter(void 0, void 0, void 0, function* () {
159
+ usedScreenrecord = true;
160
+ return { kill: () => undefined };
161
+ });
162
+ yield svc.openScrcpyCapture('udid-ok', () => undefined);
163
+ (0, chai_1.expect)(usedScreenrecord, 'must not fall back when scrcpy works').to.equal(false);
164
+ (0, chai_1.expect)(svc.scrcpyIncompatible.has('udid-ok')).to.equal(false);
165
+ }));
166
+ it('openScrcpyCapture falls back to screenrecord when scrcpy fails, and caches the incompatibility', () => __awaiter(void 0, void 0, void 0, function* () {
167
+ const svc = make();
168
+ let scrcpyAttempts = 0;
169
+ let screenrecordCalls = 0;
170
+ svc.startScrcpyOrThrow = () => __awaiter(void 0, void 0, void 0, function* () {
171
+ scrcpyAttempts++;
172
+ throw new Error('scrcpy closed before producing frames');
173
+ });
174
+ svc.openScreenrecordCapture = () => __awaiter(void 0, void 0, void 0, function* () {
175
+ screenrecordCalls++;
176
+ return { kill: () => undefined };
177
+ });
178
+ // First attempt: scrcpy tried, fails → screenrecord fallback + marked incompatible.
179
+ yield svc.openScrcpyCapture('udid-bad', () => undefined);
180
+ (0, chai_1.expect)(scrcpyAttempts).to.equal(1);
181
+ (0, chai_1.expect)(screenrecordCalls).to.equal(1);
182
+ (0, chai_1.expect)(svc.scrcpyIncompatible.has('udid-bad')).to.equal(true);
183
+ // Second attempt: skip scrcpy entirely, go straight to screenrecord.
184
+ yield svc.openScrcpyCapture('udid-bad', () => undefined);
185
+ (0, chai_1.expect)(scrcpyAttempts, 'scrcpy must not be retried once known-incompatible').to.equal(1);
186
+ (0, chai_1.expect)(screenrecordCalls).to.equal(2);
187
+ }));
83
188
  });
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const ScrcpyServerSession_1 = require("../../src/device-managers/android/ScrcpyServerSession");
5
+ describe('buildScrcpyServerArgs', () => {
6
+ it('builds the exact video-only app_process argv', () => {
7
+ const argv = (0, ScrcpyServerSession_1.buildScrcpyServerArgs)({ version: '2.7', jarDevicePath: ScrcpyServerSession_1.SCRCPY_DEVICE_JAR_PATH, maxSize: 1560 });
8
+ (0, chai_1.expect)(argv).to.deep.equal([
9
+ 'shell',
10
+ `CLASSPATH=${ScrcpyServerSession_1.SCRCPY_DEVICE_JAR_PATH}`,
11
+ 'app_process',
12
+ '/',
13
+ 'com.genymobile.scrcpy.Server',
14
+ '2.7',
15
+ 'tunnel_forward=true',
16
+ 'audio=false',
17
+ 'control=false',
18
+ 'video=true',
19
+ 'video_codec=h264',
20
+ 'max_size=1560',
21
+ 'video_bit_rate=4000000',
22
+ 'max_fps=30',
23
+ 'send_device_meta=false',
24
+ 'send_codec_meta=false',
25
+ 'send_frame_meta=false',
26
+ 'send_dummy_byte=true',
27
+ 'cleanup=true',
28
+ ]);
29
+ });
30
+ });
31
+ describe('scrcpyMaxSizeFromDims', () => {
32
+ it('caps the LONGER edge so the shorter edge lands near the target', () => {
33
+ // 1080x2340: short=1080 → scale 720/1080; long=2340*0.6667 ≈ 1560
34
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.scrcpyMaxSizeFromDims)(1080, 2340)).to.equal(1560);
35
+ });
36
+ it('is orientation-agnostic (landscape same result)', () => {
37
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.scrcpyMaxSizeFromDims)(2340, 1080)).to.equal(1560);
38
+ });
39
+ it('never upscales when the short edge is already below target', () => {
40
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.scrcpyMaxSizeFromDims)(480, 800)).to.equal(800);
41
+ });
42
+ it('returns safe default (1440) for non-finite or non-positive dimensions', () => {
43
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.scrcpyMaxSizeFromDims)(NaN, 800)).to.equal(1440);
44
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.scrcpyMaxSizeFromDims)(0, 800)).to.equal(1440);
45
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.scrcpyMaxSizeFromDims)(-100, 800)).to.equal(1440);
46
+ });
47
+ });
48
+ describe('parseAdbForwardPort', () => {
49
+ it('reads the assigned port from `adb forward tcp:0` output', () => {
50
+ (0, chai_1.expect)((0, ScrcpyServerSession_1.parseAdbForwardPort)('41337\n')).to.equal(41337);
51
+ });
52
+ it('throws on non-numeric output', () => {
53
+ (0, chai_1.expect)(() => (0, ScrcpyServerSession_1.parseAdbForwardPort)('error: device offline')).to.throw();
54
+ });
55
+ it('throws on zero or negative port numbers', () => {
56
+ (0, chai_1.expect)(() => (0, ScrcpyServerSession_1.parseAdbForwardPort)('0')).to.throw();
57
+ (0, chai_1.expect)(() => (0, ScrcpyServerSession_1.parseAdbForwardPort)('-1')).to.throw();
58
+ });
59
+ });
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const scrcpyVersion_1 = require("../../src/device-managers/android/scrcpyVersion");
9
+ describe('scrcpyVersion', () => {
10
+ it('derives the jar filename from the single version constant', () => {
11
+ (0, chai_1.expect)((0, scrcpyVersion_1.scrcpyServerJarFilename)()).to.equal(`scrcpy-server-${scrcpyVersion_1.SCRCPY_SERVER_VERSION}.jar`);
12
+ });
13
+ it('the vendored jar exists on disk at the resolved path', () => {
14
+ (0, chai_1.expect)(fs_1.default.existsSync((0, scrcpyVersion_1.scrcpyServerJarPath)()), (0, scrcpyVersion_1.scrcpyServerJarPath)()).to.equal(true);
15
+ });
16
+ });
@@ -1 +1 @@
1
- {"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/audit.ts","../src/app/routers/auth.ts","../src/app/routers/bug-report.ts","../src/app/routers/build-export.ts","../src/app/routers/capabilities.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/interceptor.ts","../src/app/routers/ports.ts","../src/app/routers/processes.ts","../src/app/routers/profile.ts","../src/app/routers/profileIdentity.ts","../src/app/routers/projects.ts","../src/app/routers/recordings.ts","../src/app/routers/reservation.ts","../src/app/routers/sdk-leases.ts","../src/app/routers/sdk-version.ts","../src/app/routers/streamType.ts","../src/app/routers/teams.ts","../src/app/routers/users.ts","../src/app/routers/webhook.ts","../src/app/ws/h264StreamWs.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidH264StreamService.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/android/H264Multiplexer.ts","../src/device-managers/android/h264NalParser.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/device-managers/ios/iosBattery.ts","../src/device-managers/ios/iosStreamDiagnostics.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/index.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/authMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/leaseTokenMiddleware.ts","../src/middleware/loginRateLimiter.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/roleGuard.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EmailService.ts","../src/services/EventBus.ts","../src/services/EventLogService.ts","../src/services/InspectorService.ts","../src/services/InterceptorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PasswordResetService.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/SelectorStateService.ts","../src/services/SelectorVerificationJob.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/UserService.ts","../src/services/UserSessionService.ts","../src/services/VideoPipelineService.ts","../src/services/sessionTokenGate.ts","../src/services/artifacts/ArtifactStore.ts","../src/services/autowait/AutowaitService.ts","../src/services/autowait/waitFor.ts","../src/services/bug-report/BugReportService.ts","../src/services/bug-report/archive.ts","../src/services/bug-report/har-collector.ts","../src/services/bug-report/manifest.ts","../src/services/bug-report/readme.ts","../src/services/bug-report/types.ts","../src/services/bug-report/video-slice.ts","../src/services/bug-report/window.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/identity/bootstrap.ts","../src/services/identity/sessionCleanupCron.ts","../src/services/identity/userAuthorization.ts","../src/services/interceptor/AndroidProxyAdapter.ts","../src/services/interceptor/CertManager.ts","../src/services/interceptor/HarBuilder.ts","../src/services/interceptor/HostFilter.ts","../src/services/interceptor/MitmProxyHost.ts","../src/services/interceptor/MockEngine.ts","../src/services/interceptor/RequestBuffer.ts","../src/services/interceptor/SessionArchive.ts","../src/services/interceptor/types.ts","../src/services/lease/LeaseOrphanSweeper.ts","../src/services/lease/LeaseService.ts","../src/services/lease/buildCapabilityBag.ts","../src/services/lease/leaseToken.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/services/ports/PortAllocatorClient.ts","../src/services/ports/PortAllocatorService.ts","../src/services/recording/RecordingOrchestrator.ts","../src/services/recording/annotation-render.ts","../src/services/recording/busy-precheck.ts","../src/services/recording/concurrency-gate.ts","../src/services/recording/manualLock.ts","../src/services/recording/proof-bundle.ts","../src/services/recording/recording-store.ts","../src/services/telemetry/attributes.ts","../src/services/token/JwtKeyService.ts","../src/services/token/StreamTicketService.ts","../src/services/token/mcpScopes.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/types/identity.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/seedUser.ts","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/audit-ingest.spec.ts","../test/integration/auth-flow.spec.ts","../test/integration/auth-rate-limit.spec.ts","../test/integration/bug-report-route.spec.ts","../test/integration/cliArgs.js","../test/integration/cross-workflow.spec.ts","../test/integration/forgot-password-rate-limit.spec.ts","../test/integration/forgot-password.spec.ts","../test/integration/healing-events-sessionfilter.spec.ts","../test/integration/legacy-deprecation-headers.spec.ts","../test/integration/migration.spec.ts","../test/integration/node-pair-auth-rest.spec.ts","../test/integration/ports-router.spec.ts","../test/integration/profile-tokens.spec.ts","../test/integration/recordings-router.spec.ts","../test/integration/reset-revokes-sessions.spec.ts","../test/integration/role-matrix.spec.ts","../test/integration/sdk-leases-router.spec.ts","../test/integration/sdk-version.spec.ts","../test/integration/selector-health-endpoint.spec.ts","../test/integration/selector-state-flow.spec.ts","../test/integration/team-membership-migration.spec.ts","../test/integration/team-visibility-dashboard.spec.ts","../test/integration/team-visibility-grid.spec.ts","../test/integration/team-visibility-recordings.spec.ts","../test/integration/users-flow.spec.ts","../test/integration/users-lockout.spec.ts","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ArtifactStore.spec.ts","../test/unit/AutowaitService.spec.ts","../test/unit/CapabilitiesRouter.spec.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/CommandInterceptor.autowait.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/EmailService.test.ts","../test/unit/EventLogService.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/JwtKeyService.spec.ts","../test/unit/McpScopes.spec.ts","../test/unit/OrphanSweeper.test.ts","../test/unit/PasswordResetService.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/ProjectsRouter.spec.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/SessionLifecycleCaps.spec.ts","../test/unit/SessionTokenGate.spec.ts","../test/unit/StreamTicketService.spec.ts","../test/unit/TeamService.test.ts","../test/unit/TokenEndpoint.spec.ts","../test/unit/UserService.test.ts","../test/unit/UserSessionService.test.ts","../test/unit/XenonCapabilityManager.spec.ts","../test/unit/aggregate-hotspots-tuple-key.spec.ts","../test/unit/android-h264-service.spec.ts","../test/unit/android-stream-adb-path.spec.ts","../test/unit/android-stream-encoder.spec.ts","../test/unit/android-stream-fast-start.spec.ts","../test/unit/android-stream-release.spec.ts","../test/unit/auth-me-auth-disabled.test.ts","../test/unit/authMiddleware.bearer.spec.ts","../test/unit/authMiddleware.mcpAudience.spec.ts","../test/unit/authMiddleware.streamTicket.spec.ts","../test/unit/authMiddleware.test.ts","../test/unit/bootstrap-identity.test.ts","../test/unit/build-export.spec.ts","../test/unit/busy-precheck.spec.ts","../test/unit/commands.spec.js","../test/unit/concurrency-gate.spec.ts","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/event-manager-regression-hook.spec.ts","../test/unit/extract-access-key-token-pair.test.ts","../test/unit/findAndLockDevice-leasemerge.spec.ts","../test/unit/h264-multiplexer.spec.ts","../test/unit/h264-nal-parser.spec.ts","../test/unit/h264-ws-frame.spec.ts","../test/unit/healing-instrumentation.spec.ts","../test/unit/healing-state-endpoints.spec.ts","../test/unit/healing-tier-gate.spec.ts","../test/unit/helpers.spec.ts","../test/unit/iosBattery.spec.ts","../test/unit/iosStreamDiagnostics.spec.ts","../test/unit/loginRateLimiter.test.ts","../test/unit/manualLock.test.ts","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/profile-router.test.ts","../test/unit/profileIdentity.spec.ts","../test/unit/proof-bundle.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/recording-instrumentation.spec.ts","../test/unit/recording-orchestrator.spec.ts","../test/unit/recording-store.spec.ts","../test/unit/roleGuard.test.ts","../test/unit/run-migrations-fail-fast.test.ts","../test/unit/run-migrations.test.ts","../test/unit/selector-state-service.spec.ts","../test/unit/selector-verification-job.spec.ts","../test/unit/socketServer.bearer.spec.ts","../test/unit/stream-status-type.spec.ts","../test/unit/team-filter.test.ts","../test/unit/tracing-service-init.spec.ts","../test/unit/userAuthorization.test.ts","../test/unit/users-router.test.ts","../test/unit/video-pipeline-args.spec.ts","../test/unit/video-pipeline-outputpath.spec.ts","../test/unit/bug-report/archive.spec.ts","../test/unit/bug-report/har-collector.spec.ts","../test/unit/bug-report/manifest.spec.ts","../test/unit/bug-report/readme.spec.ts","../test/unit/bug-report/route.spec.ts","../test/unit/bug-report/service.spec.ts","../test/unit/bug-report/video-slice.spec.ts","../test/unit/bug-report/window.spec.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js","../test/unit/interceptor/AndroidProxyAdapter.spec.ts","../test/unit/interceptor/CertManager.spec.ts","../test/unit/interceptor/HarBuilder.spec.ts","../test/unit/interceptor/HostFilter.spec.ts","../test/unit/interceptor/MitmProxyHost.spec.ts","../test/unit/interceptor/MockEngine.spec.ts","../test/unit/interceptor/RequestBuffer.spec.ts","../test/unit/interceptor/SessionArchive.spec.ts","../test/unit/lease/LeaseOrphanSweeper.spec.ts","../test/unit/lease/LeaseService.spec.ts","../test/unit/lease/buildCapabilityBag.spec.ts","../test/unit/lease/leaseToken.spec.ts","../test/unit/lease/nodePairAuth.spec.ts","../test/unit/ports/PortAllocatorService.spec.ts"],"version":"5.9.3"}
1
+ {"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/androidH264Config.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/audit.ts","../src/app/routers/auth.ts","../src/app/routers/bug-report.ts","../src/app/routers/build-export.ts","../src/app/routers/capabilities.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/interceptor.ts","../src/app/routers/ports.ts","../src/app/routers/processes.ts","../src/app/routers/profile.ts","../src/app/routers/profileIdentity.ts","../src/app/routers/projects.ts","../src/app/routers/recordings.ts","../src/app/routers/reservation.ts","../src/app/routers/sdk-leases.ts","../src/app/routers/sdk-version.ts","../src/app/routers/streamType.ts","../src/app/routers/teams.ts","../src/app/routers/users.ts","../src/app/routers/webhook.ts","../src/app/ws/h264StreamWs.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidH264StreamService.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/android/H264Multiplexer.ts","../src/device-managers/android/ScrcpyServerSession.ts","../src/device-managers/android/h264NalParser.ts","../src/device-managers/android/scrcpyVersion.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/device-managers/ios/iosBattery.ts","../src/device-managers/ios/iosStreamDiagnostics.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/index.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/authMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/leaseTokenMiddleware.ts","../src/middleware/loginRateLimiter.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/roleGuard.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EmailService.ts","../src/services/EventBus.ts","../src/services/EventLogService.ts","../src/services/InspectorService.ts","../src/services/InterceptorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PasswordResetService.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/SelectorStateService.ts","../src/services/SelectorVerificationJob.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/UserService.ts","../src/services/UserSessionService.ts","../src/services/VideoPipelineService.ts","../src/services/sessionTokenGate.ts","../src/services/artifacts/ArtifactStore.ts","../src/services/autowait/AutowaitService.ts","../src/services/autowait/waitFor.ts","../src/services/bug-report/BugReportService.ts","../src/services/bug-report/archive.ts","../src/services/bug-report/har-collector.ts","../src/services/bug-report/manifest.ts","../src/services/bug-report/readme.ts","../src/services/bug-report/types.ts","../src/services/bug-report/video-slice.ts","../src/services/bug-report/window.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/identity/bootstrap.ts","../src/services/identity/sessionCleanupCron.ts","../src/services/identity/userAuthorization.ts","../src/services/interceptor/AndroidProxyAdapter.ts","../src/services/interceptor/CertManager.ts","../src/services/interceptor/HarBuilder.ts","../src/services/interceptor/HostFilter.ts","../src/services/interceptor/MitmProxyHost.ts","../src/services/interceptor/MockEngine.ts","../src/services/interceptor/RequestBuffer.ts","../src/services/interceptor/SessionArchive.ts","../src/services/interceptor/types.ts","../src/services/lease/LeaseOrphanSweeper.ts","../src/services/lease/LeaseService.ts","../src/services/lease/buildCapabilityBag.ts","../src/services/lease/leaseToken.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/services/ports/PortAllocatorClient.ts","../src/services/ports/PortAllocatorService.ts","../src/services/recording/RecordingOrchestrator.ts","../src/services/recording/annotation-render.ts","../src/services/recording/busy-precheck.ts","../src/services/recording/concurrency-gate.ts","../src/services/recording/manualLock.ts","../src/services/recording/proof-bundle.ts","../src/services/recording/recording-store.ts","../src/services/telemetry/attributes.ts","../src/services/token/JwtKeyService.ts","../src/services/token/StreamTicketService.ts","../src/services/token/mcpScopes.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/types/identity.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/seedUser.ts","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/audit-ingest.spec.ts","../test/integration/auth-flow.spec.ts","../test/integration/auth-rate-limit.spec.ts","../test/integration/bug-report-route.spec.ts","../test/integration/cliArgs.js","../test/integration/cross-workflow.spec.ts","../test/integration/forgot-password-rate-limit.spec.ts","../test/integration/forgot-password.spec.ts","../test/integration/healing-events-sessionfilter.spec.ts","../test/integration/legacy-deprecation-headers.spec.ts","../test/integration/migration.spec.ts","../test/integration/node-pair-auth-rest.spec.ts","../test/integration/ports-router.spec.ts","../test/integration/profile-tokens.spec.ts","../test/integration/recordings-router.spec.ts","../test/integration/reset-revokes-sessions.spec.ts","../test/integration/role-matrix.spec.ts","../test/integration/sdk-leases-router.spec.ts","../test/integration/sdk-version.spec.ts","../test/integration/selector-health-endpoint.spec.ts","../test/integration/selector-state-flow.spec.ts","../test/integration/team-membership-migration.spec.ts","../test/integration/team-visibility-dashboard.spec.ts","../test/integration/team-visibility-grid.spec.ts","../test/integration/team-visibility-recordings.spec.ts","../test/integration/users-flow.spec.ts","../test/integration/users-lockout.spec.ts","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ArtifactStore.spec.ts","../test/unit/AutowaitService.spec.ts","../test/unit/CapabilitiesRouter.spec.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/CommandInterceptor.autowait.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/EmailService.test.ts","../test/unit/EventLogService.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/JwtKeyService.spec.ts","../test/unit/McpScopes.spec.ts","../test/unit/OrphanSweeper.test.ts","../test/unit/PasswordResetService.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/ProjectsRouter.spec.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/SessionLifecycleCaps.spec.ts","../test/unit/SessionTokenGate.spec.ts","../test/unit/StreamTicketService.spec.ts","../test/unit/TeamService.test.ts","../test/unit/TokenEndpoint.spec.ts","../test/unit/UserService.test.ts","../test/unit/UserSessionService.test.ts","../test/unit/XenonCapabilityManager.spec.ts","../test/unit/aggregate-hotspots-tuple-key.spec.ts","../test/unit/android-h264-config.spec.ts","../test/unit/android-h264-service.spec.ts","../test/unit/android-stream-adb-path.spec.ts","../test/unit/android-stream-encoder.spec.ts","../test/unit/android-stream-fast-start.spec.ts","../test/unit/android-stream-release.spec.ts","../test/unit/auth-me-auth-disabled.test.ts","../test/unit/authMiddleware.bearer.spec.ts","../test/unit/authMiddleware.mcpAudience.spec.ts","../test/unit/authMiddleware.streamTicket.spec.ts","../test/unit/authMiddleware.test.ts","../test/unit/bootstrap-identity.test.ts","../test/unit/build-export.spec.ts","../test/unit/busy-precheck.spec.ts","../test/unit/commands.spec.js","../test/unit/concurrency-gate.spec.ts","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/event-manager-regression-hook.spec.ts","../test/unit/extract-access-key-token-pair.test.ts","../test/unit/findAndLockDevice-leasemerge.spec.ts","../test/unit/h264-multiplexer.spec.ts","../test/unit/h264-nal-parser.spec.ts","../test/unit/h264-ws-frame.spec.ts","../test/unit/healing-instrumentation.spec.ts","../test/unit/healing-state-endpoints.spec.ts","../test/unit/healing-tier-gate.spec.ts","../test/unit/helpers.spec.ts","../test/unit/iosBattery.spec.ts","../test/unit/iosStreamDiagnostics.spec.ts","../test/unit/loginRateLimiter.test.ts","../test/unit/manualLock.test.ts","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/profile-router.test.ts","../test/unit/profileIdentity.spec.ts","../test/unit/proof-bundle.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/recording-instrumentation.spec.ts","../test/unit/recording-orchestrator.spec.ts","../test/unit/recording-store.spec.ts","../test/unit/roleGuard.test.ts","../test/unit/run-migrations-fail-fast.test.ts","../test/unit/run-migrations.test.ts","../test/unit/scrcpy-server-session.spec.ts","../test/unit/scrcpy-version.spec.ts","../test/unit/selector-state-service.spec.ts","../test/unit/selector-verification-job.spec.ts","../test/unit/socketServer.bearer.spec.ts","../test/unit/stream-status-type.spec.ts","../test/unit/team-filter.test.ts","../test/unit/tracing-service-init.spec.ts","../test/unit/userAuthorization.test.ts","../test/unit/users-router.test.ts","../test/unit/video-pipeline-args.spec.ts","../test/unit/video-pipeline-outputpath.spec.ts","../test/unit/bug-report/archive.spec.ts","../test/unit/bug-report/har-collector.spec.ts","../test/unit/bug-report/manifest.spec.ts","../test/unit/bug-report/readme.spec.ts","../test/unit/bug-report/route.spec.ts","../test/unit/bug-report/service.spec.ts","../test/unit/bug-report/video-slice.spec.ts","../test/unit/bug-report/window.spec.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js","../test/unit/interceptor/AndroidProxyAdapter.spec.ts","../test/unit/interceptor/CertManager.spec.ts","../test/unit/interceptor/HarBuilder.spec.ts","../test/unit/interceptor/HostFilter.spec.ts","../test/unit/interceptor/MitmProxyHost.spec.ts","../test/unit/interceptor/MockEngine.spec.ts","../test/unit/interceptor/RequestBuffer.spec.ts","../test/unit/interceptor/SessionArchive.spec.ts","../test/unit/lease/LeaseOrphanSweeper.spec.ts","../test/unit/lease/LeaseService.spec.ts","../test/unit/lease/buildCapabilityBag.spec.ts","../test/unit/lease/leaseToken.spec.ts","../test/unit/lease/nodePairAuth.spec.ts","../test/unit/ports/PortAllocatorService.spec.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.9.2",
3
+ "version": "1.10.1",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
@@ -12,7 +12,7 @@
12
12
  "postinstall": "node scripts/generate-prisma.js",
13
13
  "build": "node scripts/generate-prisma.js && tsc -b && npm run build:copy",
14
14
  "build:all": "npm run build:xenon && npm run build",
15
- "build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/",
15
+ "build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/ && mkdir -p lib/src/device-managers/android/vendor && cp src/device-managers/android/vendor/*.jar lib/src/device-managers/android/vendor/",
16
16
  "build:schema": "node scripts/generate-types-from-schema.js",
17
17
  "build:xenon": "sh scripts/build-xenon.sh",
18
18
  "dev": "npm run db:migrate && npm run build && npm run plugin:reinstall && npm run server",
package/schema.json CHANGED
@@ -475,9 +475,16 @@
475
475
  "description": "Live-streaming options.",
476
476
  "properties": {
477
477
  "androidH264": {
478
- "type": "boolean",
479
478
  "default": false,
480
- "description": "Use scrcpy H.264 for Android live preview (WebCodecs in the browser) instead of the MJPEG screencap loop. Falls back to MJPEG when unsupported."
479
+ "description": "Android live preview: false/omitted = MJPEG; true = H.264 via scrcpy; { \"source\": \"scrcpy\" | \"screenrecord\" } to pick the capture source explicitly. Falls back to MJPEG when unsupported.",
480
+ "oneOf": [
481
+ { "type": "boolean" },
482
+ {
483
+ "type": "object",
484
+ "additionalProperties": false,
485
+ "properties": { "source": { "type": "string", "enum": ["scrcpy", "screenrecord"] } }
486
+ }
487
+ ]
481
488
  }
482
489
  }
483
490
  }