@xenon-device-management/xenon 1.10.0 → 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 +1 -1
- package/lib/src/app/routers/androidH264Config.js +20 -7
- package/lib/src/device-managers/android/AndroidH264StreamService.js +75 -13
- package/lib/src/device-managers/android/ScrcpyServerSession.js +1 -1
- package/lib/src/device-managers/android/scrcpyVersion.js +1 -1
- package/lib/src/device-managers/android/vendor/scrcpy-server-2.7.jar +0 -0
- package/lib/test/unit/android-h264-config.spec.js +17 -0
- package/lib/test/unit/android-h264-service.spec.js +35 -0
- package/lib/test/unit/scrcpy-server-session.spec.js +2 -2
- package/package.json +1 -1
- package/lib/src/device-managers/android/vendor/scrcpy-server-3.3.4.jar +0 -0
package/lib/package.json
CHANGED
|
@@ -2,15 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveAndroidH264 = resolveAndroidH264;
|
|
4
4
|
/**
|
|
5
|
-
* Normalize the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
18
|
*/
|
|
10
19
|
function resolveAndroidH264(cfg) {
|
|
11
|
-
if (!cfg)
|
|
20
|
+
if (!cfg || cfg === 'false')
|
|
12
21
|
return { enabled: false, source: 'scrcpy' };
|
|
13
|
-
if (cfg === true)
|
|
22
|
+
if (cfg === true || cfg === 'true')
|
|
14
23
|
return { enabled: true, source: 'scrcpy' };
|
|
15
|
-
|
|
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' };
|
|
16
29
|
}
|
|
@@ -83,6 +83,10 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
83
83
|
constructor() {
|
|
84
84
|
this.sessions = new Map();
|
|
85
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();
|
|
86
90
|
this.startWatchdog();
|
|
87
91
|
}
|
|
88
92
|
startWatchdog() {
|
|
@@ -197,26 +201,84 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
197
201
|
* a crash and stop the session rather than silently going dark.
|
|
198
202
|
*/
|
|
199
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) {
|
|
200
232
|
return __awaiter(this, void 0, void 0, function* () {
|
|
201
233
|
const { ScrcpyServerSession, scrcpyMaxSizeFromDims } = yield Promise.resolve().then(() => __importStar(require('./ScrcpyServerSession')));
|
|
202
234
|
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
203
235
|
const maxSize = scrcpyMaxSizeFromDims(Number(device === null || device === void 0 ? void 0 : device.screenWidth), Number(device === null || device === void 0 ? void 0 : device.screenHeight));
|
|
204
236
|
const parser = new h264NalParser_1.H264NalParser();
|
|
205
237
|
const session = new ScrcpyServerSession(udid);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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);
|
|
217
281
|
});
|
|
218
|
-
yield session.start(maxSize);
|
|
219
|
-
return { kill: () => session.stop() };
|
|
220
282
|
});
|
|
221
283
|
}
|
|
222
284
|
/**
|
|
@@ -66,7 +66,7 @@ exports.SCRCPY_DEVICE_JAR_PATH = '/data/local/tmp/scrcpy-server-manual.jar';
|
|
|
66
66
|
* so the socket carries plain Annex-B H.264 that the existing H264NalParser
|
|
67
67
|
* consumes unchanged. `send_dummy_byte=true` (the tunnel_forward readiness byte)
|
|
68
68
|
* is explicit because the socket reader skips exactly one leading byte.
|
|
69
|
-
* Arg NAMES verified against the vendored scrcpy
|
|
69
|
+
* Arg NAMES verified against the vendored scrcpy 2.7 jar's dex; the version
|
|
70
70
|
* constant and this argv move together (see scrcpyVersion.ts / vendor/README.md).
|
|
71
71
|
* No `scid` → the server listens on `localabstract:scrcpy` (per-device namespace).
|
|
72
72
|
*/
|
|
@@ -12,7 +12,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
12
12
|
* <version> argument both derive from this — they must always agree, so this is
|
|
13
13
|
* the single source of truth. Bump procedure: see vendor/README.md.
|
|
14
14
|
*/
|
|
15
|
-
exports.SCRCPY_SERVER_VERSION = '
|
|
15
|
+
exports.SCRCPY_SERVER_VERSION = '2.7';
|
|
16
16
|
function scrcpyServerJarFilename() {
|
|
17
17
|
return `scrcpy-server-${exports.SCRCPY_SERVER_VERSION}.jar`;
|
|
18
18
|
}
|
|
Binary file
|
|
@@ -17,4 +17,21 @@ describe('resolveAndroidH264', () => {
|
|
|
17
17
|
source: 'screenrecord',
|
|
18
18
|
});
|
|
19
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
|
+
});
|
|
20
37
|
});
|
|
@@ -23,6 +23,7 @@ function make() {
|
|
|
23
23
|
const svc = Object.create(AndroidH264StreamService_1.default.prototype);
|
|
24
24
|
svc.sessions = new Map();
|
|
25
25
|
svc.startPromises = new Map();
|
|
26
|
+
svc.scrcpyIncompatible = new Set();
|
|
26
27
|
return svc;
|
|
27
28
|
}
|
|
28
29
|
describe('AndroidH264StreamService', () => {
|
|
@@ -150,4 +151,38 @@ describe('AndroidH264StreamService', () => {
|
|
|
150
151
|
yield svc.start('udid-ws-scrcpy'); // no opts
|
|
151
152
|
(0, chai_1.expect)(used).to.equal('scrcpy');
|
|
152
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
|
+
}));
|
|
153
188
|
});
|
|
@@ -4,14 +4,14 @@ const chai_1 = require("chai");
|
|
|
4
4
|
const ScrcpyServerSession_1 = require("../../src/device-managers/android/ScrcpyServerSession");
|
|
5
5
|
describe('buildScrcpyServerArgs', () => {
|
|
6
6
|
it('builds the exact video-only app_process argv', () => {
|
|
7
|
-
const argv = (0, ScrcpyServerSession_1.buildScrcpyServerArgs)({ version: '
|
|
7
|
+
const argv = (0, ScrcpyServerSession_1.buildScrcpyServerArgs)({ version: '2.7', jarDevicePath: ScrcpyServerSession_1.SCRCPY_DEVICE_JAR_PATH, maxSize: 1560 });
|
|
8
8
|
(0, chai_1.expect)(argv).to.deep.equal([
|
|
9
9
|
'shell',
|
|
10
10
|
`CLASSPATH=${ScrcpyServerSession_1.SCRCPY_DEVICE_JAR_PATH}`,
|
|
11
11
|
'app_process',
|
|
12
12
|
'/',
|
|
13
13
|
'com.genymobile.scrcpy.Server',
|
|
14
|
-
'
|
|
14
|
+
'2.7',
|
|
15
15
|
'tunnel_forward=true',
|
|
16
16
|
'audio=false',
|
|
17
17
|
'control=false',
|
package/package.json
CHANGED
|
Binary file
|