@specific.dev/spectest 0.125.0 → 0.126.0
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/dist/components/android-recording-state.mjs +28 -0
- package/dist/components/android-runtime.mjs +5 -14
- package/dist/components/android.d.ts +1 -1
- package/dist/components/android.js +23 -2
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/package.json +1 -1
- package/src/components/android-recording-state.mjs +28 -0
- package/src/components/android-runtime.mjs +5 -14
- package/src/components/android.test.ts +59 -0
- package/src/components/android.ts +24 -2
- package/src/components/index.ts +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// scrcpy opens its output before receiving codec metadata or a video frame.
|
|
2
|
+
// Only the patched recorder's first-frame signal makes an immediate stop safe.
|
|
3
|
+
export const FIRST_VIDEO_FRAME = "SPECTEST_FIRST_VIDEO_FRAME";
|
|
4
|
+
export const RECORDER_CAPABILITY = "spectest-first-frame-v1";
|
|
5
|
+
|
|
6
|
+
export function requireRecorderCapability(version) {
|
|
7
|
+
if (!version.includes(RECORDER_CAPABILITY)) {
|
|
8
|
+
throw new Error("Android toolsImage needs scrcpy with the spectest-first-frame-v1 patch; rebuild it from ANDROID_TOOLS_DOCKERFILE");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function observeRecorderOutput(record, chunk) {
|
|
13
|
+
record.log = (record.log + chunk).slice(-16000);
|
|
14
|
+
if (!record.started && record.log.includes(FIRST_VIDEO_FRAME)) {
|
|
15
|
+
record.started = true;
|
|
16
|
+
record.startedAtMs = Date.now();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function waitForRecorderFrame(record, timeoutMs = 10000) {
|
|
21
|
+
const deadline = Date.now() + timeoutMs;
|
|
22
|
+
while (true) {
|
|
23
|
+
if (record.error) throw new Error(`${record.error}: ${record.log}`);
|
|
24
|
+
if (record.started) return { startedAtMs: record.startedAtMs };
|
|
25
|
+
if (Date.now() >= deadline) throw new Error(`Recorder first video frame timed out: ${record.log}`);
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Runs inside the Android tools container, in the same snapshotted VM.
|
|
2
2
|
import http from "node:http";
|
|
3
|
+
import { requireRecorderCapability, observeRecorderOutput, waitForRecorderFrame } from "./android-recording-state.mjs";
|
|
3
4
|
import { spawn, execFile } from "node:child_process";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
import { mkdir, stat, open, rm } from "node:fs/promises";
|
|
@@ -74,6 +75,7 @@ bootstrap().catch((error) => {
|
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
async function startRecording(body) {
|
|
78
|
+
requireRecorderCapability((await exec("scrcpy", ["--version"], { timeout: 5000 })).stdout);
|
|
77
79
|
if (!/^[a-z0-9-]{1,100}$/.test(body.id)) throw new Error("invalid recording id");
|
|
78
80
|
if (recordings.has(body.id)) throw new Error("recording already exists");
|
|
79
81
|
if ([...recordings.values()].some((r) => !r.stopped))
|
|
@@ -121,19 +123,13 @@ async function startRecording(body) {
|
|
|
121
123
|
record.error = error.message;
|
|
122
124
|
resolve(-1);
|
|
123
125
|
});
|
|
124
|
-
child.on("
|
|
126
|
+
child.on("close", (code) => {
|
|
125
127
|
if (!record.stopped) record.error = `Recorder exited early (${code})`;
|
|
126
128
|
resolve(code);
|
|
127
129
|
});
|
|
128
130
|
});
|
|
129
131
|
for (const output of [child.stdout, child.stderr])
|
|
130
|
-
output.on("data", (b) =>
|
|
131
|
-
record.log = (record.log + b).slice(-16000);
|
|
132
|
-
if (!record.started && record.log.includes("Recording started")) {
|
|
133
|
-
record.started = true;
|
|
134
|
-
record.startedAtMs = Date.now();
|
|
135
|
-
}
|
|
136
|
-
});
|
|
132
|
+
output.on("data", (b) => observeRecorderOutput(record, b));
|
|
137
133
|
record.guard = setInterval(async () => {
|
|
138
134
|
if ((await stat(path).catch(() => ({ size: 0 }))).size > maxBytes) {
|
|
139
135
|
record.error = "Recording exceeded 256 MiB";
|
|
@@ -141,12 +137,7 @@ async function startRecording(body) {
|
|
|
141
137
|
}
|
|
142
138
|
}, 1000);
|
|
143
139
|
try {
|
|
144
|
-
|
|
145
|
-
if (record.error) throw new Error(`${record.error}: ${record.log}`);
|
|
146
|
-
if (record.started) return { startedAtMs: record.startedAtMs };
|
|
147
|
-
await sleep(50);
|
|
148
|
-
}
|
|
149
|
-
throw new Error(`Recorder startup timed out: ${record.log}`);
|
|
140
|
+
return await waitForRecorderFrame(record);
|
|
150
141
|
} catch (error) {
|
|
151
142
|
clearInterval(record.guard);
|
|
152
143
|
record.stopped = true;
|
|
@@ -19,7 +19,7 @@ export interface AndroidOptions {
|
|
|
19
19
|
recording?: false | AndroidRecordingOptions;
|
|
20
20
|
dependsOn?: string[];
|
|
21
21
|
}
|
|
22
|
-
export declare const ANDROID_TOOLS_DOCKERFILE
|
|
22
|
+
export declare const ANDROID_TOOLS_DOCKERFILE: string;
|
|
23
23
|
/** Native Android, Appium and software recording, all inside the environment VM. */
|
|
24
24
|
export declare function android(options: AndroidOptions): import("../index.js").ServiceGroup<{
|
|
25
25
|
device: {
|
|
@@ -4,9 +4,26 @@ import { serviceGroup } from "../index.js";
|
|
|
4
4
|
import { resolveExistingProjectPath } from "../project-files.js";
|
|
5
5
|
import { androidApp } from "../android.js";
|
|
6
6
|
import { ENVIRONMENT_CA_PATH } from "../environment-ca.js";
|
|
7
|
+
// Instrument the pinned recorder at the point its first real video packet is
|
|
8
|
+
// retained. This follows the MP4 header and survives an immediate SIGINT.
|
|
9
|
+
const recorderPatch = String.raw `from pathlib import Path
|
|
10
|
+
p = Path('/tmp/scrcpy-3.3.1/app/src/recorder.c')
|
|
11
|
+
s = p.read_text()
|
|
12
|
+
a = ' int64_t pts_origin = AV_NOPTS_VALUE;'
|
|
13
|
+
b = ' video_pkt_previous = video_pkt;'
|
|
14
|
+
assert s.count(a) == 1 and s.count(b) == 1
|
|
15
|
+
s = s.replace(a, a + '\n bool spectest_first_frame = true;')
|
|
16
|
+
s = s.replace(b, b + '\n if (spectest_first_frame) {\n LOGI("SPECTEST_FIRST_VIDEO_FRAME");\n spectest_first_frame = false;\n }')
|
|
17
|
+
p.write_text(s)
|
|
18
|
+
p = Path('/tmp/scrcpy-3.3.1/app/src/version.c')
|
|
19
|
+
s = p.read_text()
|
|
20
|
+
a = 'scrcpy_print_version(void) {'
|
|
21
|
+
assert s.count(a) == 1
|
|
22
|
+
p.write_text(s.replace(a, a + '\n puts("spectest-first-frame-v1");'))`;
|
|
23
|
+
const shellQuote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
7
24
|
export const ANDROID_TOOLS_DOCKERFILE = `FROM node:22-bookworm-slim
|
|
8
|
-
RUN apt-get update && apt-get install -y --no-install-recommends adb aapt apksigner zipalign ffmpeg default-jdk-headless ca-certificates openssl curl gcc pkg-config meson ninja-build libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev libswresample-dev libusb-1.0-0-dev && rm -rf /var/lib/apt/lists/*
|
|
9
|
-
RUN curl -fL https://github.com/Genymobile/scrcpy/archive/refs/tags/v3.3.1.tar.gz | tar xz -C /tmp && curl -fL https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 -o /tmp/scrcpy-server && meson setup /tmp/scrcpy-build /tmp/scrcpy-3.3.1 --buildtype=release -Dprebuilt_server=/tmp/scrcpy-server && ninja -C /tmp/scrcpy-build -j4 install && rm -rf /tmp/scrcpy*
|
|
25
|
+
RUN apt-get update && apt-get install -y --no-install-recommends adb aapt apksigner zipalign ffmpeg default-jdk-headless ca-certificates openssl curl gcc python3 pkg-config meson ninja-build libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev libswresample-dev libusb-1.0-0-dev && rm -rf /var/lib/apt/lists/*
|
|
26
|
+
RUN curl -fL https://github.com/Genymobile/scrcpy/archive/refs/tags/v3.3.1.tar.gz | tar xz -C /tmp && curl -fL https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 -o /tmp/scrcpy-server && python3 -c ${shellQuote(recorderPatch.replaceAll("\n", ";"))} && meson setup /tmp/scrcpy-build /tmp/scrcpy-3.3.1 --buildtype=release -Dprebuilt_server=/tmp/scrcpy-server && ninja -C /tmp/scrcpy-build -j4 install && rm -rf /tmp/scrcpy*
|
|
10
27
|
ENV ANDROID_HOME=/opt/android JAVA_HOME=/usr/lib/jvm/default-java APPIUM_HOME=/opt/appium
|
|
11
28
|
RUN mkdir -p /opt/android/platform-tools && for tool in adb aapt aapt2 apksigner zipalign; do ln -s /usr/bin/$tool /opt/android/platform-tools/$tool; done && npm install -g appium@3.8.0 && appium driver install uiautomator2@8.7.0
|
|
12
29
|
`;
|
|
@@ -62,6 +79,10 @@ export function android(options) {
|
|
|
62
79
|
path: "/spectest-android.mjs",
|
|
63
80
|
content: readFileSync(new URL("./android-runtime.mjs", import.meta.url), "utf8"),
|
|
64
81
|
},
|
|
82
|
+
{
|
|
83
|
+
path: "/android-recording-state.mjs",
|
|
84
|
+
content: readFileSync(new URL("./android-recording-state.mjs", import.meta.url), "utf8"),
|
|
85
|
+
},
|
|
65
86
|
{
|
|
66
87
|
path: "/spectest-android-trust.mjs",
|
|
67
88
|
content: readFileSync(new URL("./android-trust.mjs", import.meta.url), "utf8"),
|
|
@@ -15,4 +15,4 @@ export { microsoft, type MicrosoftOptions } from "./microsoft.js";
|
|
|
15
15
|
export { okta, type OktaOptions } from "./okta.js";
|
|
16
16
|
export { replayFake, type ReplayFakeOptions, type ReplayFakeContext, type ReplayHandler, type ReplayKey, type WebSocketKey, type ReplayWebSocketMessage, type CassetteWebSocket, type CassetteWebSocketMessage, type WebSocketTiming, type ReplayRequest, type MatchKey, type JsonValue, type InjectRule, type InjectMatch, type StringMatch, type SignConfig, type AwsSigV4Sign, type ReplayHelpers, type ReplaySummary, type Cassette, type CassetteInteraction, type CassetteRequest, type CassetteResponse, type CassetteState, } from "./replayFake.js";
|
|
17
17
|
export { electron, type ElectronOptions } from "./electron.js";
|
|
18
|
-
export { android, type AndroidOptions } from "./android.js";
|
|
18
|
+
export { android, ANDROID_TOOLS_DOCKERFILE, type AndroidOptions } from "./android.js";
|
package/dist/components/index.js
CHANGED
|
@@ -24,4 +24,4 @@ export { microsoft } from "./microsoft.js";
|
|
|
24
24
|
export { okta } from "./okta.js";
|
|
25
25
|
export { replayFake, } from "./replayFake.js";
|
|
26
26
|
export { electron } from "./electron.js";
|
|
27
|
-
export { android } from "./android.js";
|
|
27
|
+
export { android, ANDROID_TOOLS_DOCKERFILE } from "./android.js";
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// scrcpy opens its output before receiving codec metadata or a video frame.
|
|
2
|
+
// Only the patched recorder's first-frame signal makes an immediate stop safe.
|
|
3
|
+
export const FIRST_VIDEO_FRAME = "SPECTEST_FIRST_VIDEO_FRAME";
|
|
4
|
+
export const RECORDER_CAPABILITY = "spectest-first-frame-v1";
|
|
5
|
+
|
|
6
|
+
export function requireRecorderCapability(version) {
|
|
7
|
+
if (!version.includes(RECORDER_CAPABILITY)) {
|
|
8
|
+
throw new Error("Android toolsImage needs scrcpy with the spectest-first-frame-v1 patch; rebuild it from ANDROID_TOOLS_DOCKERFILE");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function observeRecorderOutput(record, chunk) {
|
|
13
|
+
record.log = (record.log + chunk).slice(-16000);
|
|
14
|
+
if (!record.started && record.log.includes(FIRST_VIDEO_FRAME)) {
|
|
15
|
+
record.started = true;
|
|
16
|
+
record.startedAtMs = Date.now();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function waitForRecorderFrame(record, timeoutMs = 10000) {
|
|
21
|
+
const deadline = Date.now() + timeoutMs;
|
|
22
|
+
while (true) {
|
|
23
|
+
if (record.error) throw new Error(`${record.error}: ${record.log}`);
|
|
24
|
+
if (record.started) return { startedAtMs: record.startedAtMs };
|
|
25
|
+
if (Date.now() >= deadline) throw new Error(`Recorder first video frame timed out: ${record.log}`);
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Runs inside the Android tools container, in the same snapshotted VM.
|
|
2
2
|
import http from "node:http";
|
|
3
|
+
import { requireRecorderCapability, observeRecorderOutput, waitForRecorderFrame } from "./android-recording-state.mjs";
|
|
3
4
|
import { spawn, execFile } from "node:child_process";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
import { mkdir, stat, open, rm } from "node:fs/promises";
|
|
@@ -74,6 +75,7 @@ bootstrap().catch((error) => {
|
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
async function startRecording(body) {
|
|
78
|
+
requireRecorderCapability((await exec("scrcpy", ["--version"], { timeout: 5000 })).stdout);
|
|
77
79
|
if (!/^[a-z0-9-]{1,100}$/.test(body.id)) throw new Error("invalid recording id");
|
|
78
80
|
if (recordings.has(body.id)) throw new Error("recording already exists");
|
|
79
81
|
if ([...recordings.values()].some((r) => !r.stopped))
|
|
@@ -121,19 +123,13 @@ async function startRecording(body) {
|
|
|
121
123
|
record.error = error.message;
|
|
122
124
|
resolve(-1);
|
|
123
125
|
});
|
|
124
|
-
child.on("
|
|
126
|
+
child.on("close", (code) => {
|
|
125
127
|
if (!record.stopped) record.error = `Recorder exited early (${code})`;
|
|
126
128
|
resolve(code);
|
|
127
129
|
});
|
|
128
130
|
});
|
|
129
131
|
for (const output of [child.stdout, child.stderr])
|
|
130
|
-
output.on("data", (b) =>
|
|
131
|
-
record.log = (record.log + b).slice(-16000);
|
|
132
|
-
if (!record.started && record.log.includes("Recording started")) {
|
|
133
|
-
record.started = true;
|
|
134
|
-
record.startedAtMs = Date.now();
|
|
135
|
-
}
|
|
136
|
-
});
|
|
132
|
+
output.on("data", (b) => observeRecorderOutput(record, b));
|
|
137
133
|
record.guard = setInterval(async () => {
|
|
138
134
|
if ((await stat(path).catch(() => ({ size: 0 }))).size > maxBytes) {
|
|
139
135
|
record.error = "Recording exceeded 256 MiB";
|
|
@@ -141,12 +137,7 @@ async function startRecording(body) {
|
|
|
141
137
|
}
|
|
142
138
|
}, 1000);
|
|
143
139
|
try {
|
|
144
|
-
|
|
145
|
-
if (record.error) throw new Error(`${record.error}: ${record.log}`);
|
|
146
|
-
if (record.started) return { startedAtMs: record.startedAtMs };
|
|
147
|
-
await sleep(50);
|
|
148
|
-
}
|
|
149
|
-
throw new Error(`Recorder startup timed out: ${record.log}`);
|
|
140
|
+
return await waitForRecorderFrame(record);
|
|
150
141
|
} catch (error) {
|
|
151
142
|
clearInterval(record.guard);
|
|
152
143
|
record.stopped = true;
|
|
@@ -68,3 +68,62 @@ test("bad dimensions and identifiers fail before image builds", () => {
|
|
|
68
68
|
android({ apk: "spectest/tests/app.apk", package: "com.example", activity: ".MainActivity" }),
|
|
69
69
|
).toThrow("invalidate snapshots");
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
test("recorder waits for a retained first frame before an immediate stop, including split output", async () => {
|
|
73
|
+
const { spawn } = await import("node:child_process");
|
|
74
|
+
const { observeRecorderOutput, waitForRecorderFrame } = await import("./android-recording-state.mjs");
|
|
75
|
+
// Opening the output is deliberately separate from receiving the frame.
|
|
76
|
+
// This models scrcpy's static-screen path: only SIGINT flushes the last frame.
|
|
77
|
+
const child = spawn("node", ["-e", `
|
|
78
|
+
let retained = false;
|
|
79
|
+
process.on('SIGINT', () => {
|
|
80
|
+
process.stdout.write(retained ? 'VALID_VIDEO_WITH_TRAILER' : 'INVALID_EMPTY_VIDEO');
|
|
81
|
+
process.exit(retained ? 0 : 1);
|
|
82
|
+
});
|
|
83
|
+
process.stdin.once('data', () => {
|
|
84
|
+
retained = true;
|
|
85
|
+
process.stdout.write('SPECTEST_FIRST_VIDEO_');
|
|
86
|
+
setImmediate(() => process.stdout.write('FRAME\\n'));
|
|
87
|
+
});
|
|
88
|
+
process.stdout.write('Recording started to mp4 file: fixture.mp4\\n');
|
|
89
|
+
`]);
|
|
90
|
+
const record = { log: "", error: null as string | null, started: false, startedAtMs: 0 };
|
|
91
|
+
let opened!: () => void;
|
|
92
|
+
const outputOpened = new Promise<void>(resolve => { opened = resolve; });
|
|
93
|
+
child.stdout.on("data", chunk => {
|
|
94
|
+
observeRecorderOutput(record, chunk);
|
|
95
|
+
if (record.log.includes("Recording started")) opened();
|
|
96
|
+
});
|
|
97
|
+
const closed = new Promise<number | null>(resolve => child.on("close", resolve));
|
|
98
|
+
try {
|
|
99
|
+
await outputOpened;
|
|
100
|
+
let ready = false;
|
|
101
|
+
const waiting = waitForRecorderFrame(record).then(value => { ready = true; return value; });
|
|
102
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
103
|
+
expect(ready).toBe(false);
|
|
104
|
+
expect(record.started).toBe(false);
|
|
105
|
+
const frameRequested = Date.now();
|
|
106
|
+
child.stdin.write("deliver first frame");
|
|
107
|
+
const result = await waiting;
|
|
108
|
+
expect(result.startedAtMs).toBeGreaterThanOrEqual(frameRequested);
|
|
109
|
+
child.kill("SIGINT");
|
|
110
|
+
expect(await closed).toBe(0);
|
|
111
|
+
expect(record.log).toContain("VALID_VIDEO_WITH_TRAILER");
|
|
112
|
+
expect(record.log).not.toContain("INVALID_EMPTY_VIDEO");
|
|
113
|
+
} finally {
|
|
114
|
+
child.kill("SIGKILL");
|
|
115
|
+
await closed;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("recorder rejects missing frames, early exit and unpatched custom tools explicitly", async () => {
|
|
120
|
+
const { requireRecorderCapability, observeRecorderOutput, waitForRecorderFrame } = await import("./android-recording-state.mjs");
|
|
121
|
+
expect(() => requireRecorderCapability("scrcpy 3.3.1\n")).toThrow("rebuild it from ANDROID_TOOLS_DOCKERFILE");
|
|
122
|
+
expect(() => requireRecorderCapability("scrcpy 3.3.1\nspectest-first-frame-v1\n")).not.toThrow();
|
|
123
|
+
const record = { log: "", error: null as string | null, started: false, startedAtMs: 0 };
|
|
124
|
+
observeRecorderOutput(record, "Recording started to mp4 file: empty.mp4\n");
|
|
125
|
+
await expect(waitForRecorderFrame(record, 20)).rejects.toThrow("first video frame timed out");
|
|
126
|
+
record.error = "Recorder exited early (1)";
|
|
127
|
+
observeRecorderOutput(record, "SPECTEST_FIRST_VIDEO_FRAME\n");
|
|
128
|
+
await expect(waitForRecorderFrame(record)).rejects.toThrow("Recorder exited early");
|
|
129
|
+
});
|
|
@@ -26,9 +26,27 @@ export interface AndroidOptions {
|
|
|
26
26
|
dependsOn?: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Instrument the pinned recorder at the point its first real video packet is
|
|
30
|
+
// retained. This follows the MP4 header and survives an immediate SIGINT.
|
|
31
|
+
const recorderPatch = String.raw`from pathlib import Path
|
|
32
|
+
p = Path('/tmp/scrcpy-3.3.1/app/src/recorder.c')
|
|
33
|
+
s = p.read_text()
|
|
34
|
+
a = ' int64_t pts_origin = AV_NOPTS_VALUE;'
|
|
35
|
+
b = ' video_pkt_previous = video_pkt;'
|
|
36
|
+
assert s.count(a) == 1 and s.count(b) == 1
|
|
37
|
+
s = s.replace(a, a + '\n bool spectest_first_frame = true;')
|
|
38
|
+
s = s.replace(b, b + '\n if (spectest_first_frame) {\n LOGI("SPECTEST_FIRST_VIDEO_FRAME");\n spectest_first_frame = false;\n }')
|
|
39
|
+
p.write_text(s)
|
|
40
|
+
p = Path('/tmp/scrcpy-3.3.1/app/src/version.c')
|
|
41
|
+
s = p.read_text()
|
|
42
|
+
a = 'scrcpy_print_version(void) {'
|
|
43
|
+
assert s.count(a) == 1
|
|
44
|
+
p.write_text(s.replace(a, a + '\n puts("spectest-first-frame-v1");'))`;
|
|
45
|
+
const shellQuote = (value: string) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
46
|
+
|
|
29
47
|
export const ANDROID_TOOLS_DOCKERFILE = `FROM node:22-bookworm-slim
|
|
30
|
-
RUN apt-get update && apt-get install -y --no-install-recommends adb aapt apksigner zipalign ffmpeg default-jdk-headless ca-certificates openssl curl gcc pkg-config meson ninja-build libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev libswresample-dev libusb-1.0-0-dev && rm -rf /var/lib/apt/lists/*
|
|
31
|
-
RUN curl -fL https://github.com/Genymobile/scrcpy/archive/refs/tags/v3.3.1.tar.gz | tar xz -C /tmp && curl -fL https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 -o /tmp/scrcpy-server && meson setup /tmp/scrcpy-build /tmp/scrcpy-3.3.1 --buildtype=release -Dprebuilt_server=/tmp/scrcpy-server && ninja -C /tmp/scrcpy-build -j4 install && rm -rf /tmp/scrcpy*
|
|
48
|
+
RUN apt-get update && apt-get install -y --no-install-recommends adb aapt apksigner zipalign ffmpeg default-jdk-headless ca-certificates openssl curl gcc python3 pkg-config meson ninja-build libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev libswresample-dev libusb-1.0-0-dev && rm -rf /var/lib/apt/lists/*
|
|
49
|
+
RUN curl -fL https://github.com/Genymobile/scrcpy/archive/refs/tags/v3.3.1.tar.gz | tar xz -C /tmp && curl -fL https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 -o /tmp/scrcpy-server && python3 -c ${shellQuote(recorderPatch.replaceAll("\n", ";"))} && meson setup /tmp/scrcpy-build /tmp/scrcpy-3.3.1 --buildtype=release -Dprebuilt_server=/tmp/scrcpy-server && ninja -C /tmp/scrcpy-build -j4 install && rm -rf /tmp/scrcpy*
|
|
32
50
|
ENV ANDROID_HOME=/opt/android JAVA_HOME=/usr/lib/jvm/default-java APPIUM_HOME=/opt/appium
|
|
33
51
|
RUN mkdir -p /opt/android/platform-tools && for tool in adb aapt aapt2 apksigner zipalign; do ln -s /usr/bin/$tool /opt/android/platform-tools/$tool; done && npm install -g appium@3.8.0 && appium driver install uiautomator2@8.7.0
|
|
34
52
|
`;
|
|
@@ -92,6 +110,10 @@ export function android(options: AndroidOptions) {
|
|
|
92
110
|
path: "/spectest-android.mjs",
|
|
93
111
|
content: readFileSync(new URL("./android-runtime.mjs", import.meta.url), "utf8"),
|
|
94
112
|
},
|
|
113
|
+
{
|
|
114
|
+
path: "/android-recording-state.mjs",
|
|
115
|
+
content: readFileSync(new URL("./android-recording-state.mjs", import.meta.url), "utf8"),
|
|
116
|
+
},
|
|
95
117
|
{
|
|
96
118
|
path: "/spectest-android-trust.mjs",
|
|
97
119
|
content: readFileSync(new URL("./android-trust.mjs", import.meta.url), "utf8"),
|
package/src/components/index.ts
CHANGED
|
@@ -111,4 +111,4 @@ export {
|
|
|
111
111
|
} from "./replayFake.js";
|
|
112
112
|
export { electron, type ElectronOptions } from "./electron.js";
|
|
113
113
|
|
|
114
|
-
export { android, type AndroidOptions } from "./android.js";
|
|
114
|
+
export { android, ANDROID_TOOLS_DOCKERFILE, type AndroidOptions } from "./android.js";
|