@appium/coresim 1.5.0 → 1.7.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/CHANGELOG.md +12 -0
- package/README.md +14 -0
- package/binding.gyp +7 -0
- package/lib/src/commands/video-recording.d.ts +26 -10
- package/lib/src/commands/video-recording.d.ts.map +1 -1
- package/lib/src/commands/video-recording.js +55 -16
- package/lib/src/commands/video-recording.js.map +1 -1
- package/lib/src/commands/video-stream.d.ts +55 -7
- package/lib/src/commands/video-stream.d.ts.map +1 -1
- package/lib/src/commands/video-stream.js +37 -35
- package/lib/src/commands/video-stream.js.map +1 -1
- package/lib/src/index.d.ts +1 -1
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/native-simctl.js +12 -1
- package/lib/src/native-simctl.js.map +1 -1
- package/lib/src/types.d.ts +82 -10
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/utils/index.d.ts +1 -1
- package/lib/src/utils/index.d.ts.map +1 -1
- package/lib/src/utils/index.js +1 -1
- package/lib/src/utils/index.js.map +1 -1
- package/lib/src/utils/run-catching.d.ts +4 -0
- package/lib/src/utils/run-catching.d.ts.map +1 -1
- package/lib/src/utils/run-catching.js +11 -0
- package/lib/src/utils/run-catching.js.map +1 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
- package/src/commands/video-recording.ts +75 -25
- package/src/commands/video-stream.ts +48 -39
- package/src/coresim.mm +547 -118
- package/src/index.ts +1 -0
- package/src/native/audio_encoder.h +65 -0
- package/src/native/audio_encoder.mm +270 -0
- package/src/native/av_recording.h +52 -0
- package/src/native/av_recording.mm +424 -0
- package/src/native/av_stream.h +70 -0
- package/src/native/av_stream.mm +217 -0
- package/src/native/monotonic_clock.h +20 -0
- package/src/native/sim_audio_tap.h +67 -0
- package/src/native/sim_audio_tap.mm +350 -0
- package/src/native/sim_process.h +10 -0
- package/src/native/sim_process.mm +81 -18
- package/src/native/sim_video_stream.h +14 -11
- package/src/native/sim_video_stream.mm +27 -383
- package/src/native/video_encoder.h +76 -0
- package/src/native/video_encoder.mm +417 -0
- package/src/native-simctl.ts +12 -1
- package/src/types.ts +95 -11
- package/src/utils/index.ts +1 -1
- package/src/utils/run-catching.ts +11 -0
package/lib/src/types.d.ts
CHANGED
|
@@ -116,9 +116,43 @@ export interface VideoRecordingOptions {
|
|
|
116
116
|
/**
|
|
117
117
|
* For a non-rectangular display (e.g. a Dynamic Island cutout): `'ignored'` (default) saves the
|
|
118
118
|
* unmasked framebuffer, `'black'` renders the mask black, `'alpha'` is not supported and
|
|
119
|
-
* behaves like `'black'`.
|
|
119
|
+
* behaves like `'black'`. Only applies when neither `audio` nor `fps` is set — see their doc
|
|
120
|
+
* comments.
|
|
120
121
|
*/
|
|
121
122
|
mask?: 'ignored' | 'alpha' | 'black';
|
|
123
|
+
/**
|
|
124
|
+
* Also capture the device's audio into the same file, muxed as a second track. Defaults to
|
|
125
|
+
* `false`. Requires macOS 14.2+ (Core Audio process taps), the host's "System Audio Recording
|
|
126
|
+
* Only" privacy permission (System Settings > Privacy & Security — cannot be granted
|
|
127
|
+
* programmatically; a denial isn't a thrown error, it surfaces as a silent, audio-less/near-
|
|
128
|
+
* silent recording), a default audio output device on the host, and a booted device that has
|
|
129
|
+
* produced audio at least once. See the README's "Screen capture" section for the full
|
|
130
|
+
* requirements list and known failure modes.
|
|
131
|
+
*
|
|
132
|
+
* Like an explicit `fps`, this switches the implementation to this addon's own VideoToolbox +
|
|
133
|
+
* Core Audio encoders instead of CoreSimulator's private recorder, which can't mux audio.
|
|
134
|
+
*/
|
|
135
|
+
audio?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Max frames/sec to poll the framebuffer at — see {@link VideoStreamOptions} `fps` for the
|
|
138
|
+
* identical semantics. Meaningless against CoreSimulator's private recorder (it captures on its
|
|
139
|
+
* own cadence, not one we poll), so setting `fps` — even without `audio` — switches this
|
|
140
|
+
* recording to the same own-encoder implementation `audio` does. That switch costs `mask`
|
|
141
|
+
* support, which only the private recorder implements.
|
|
142
|
+
*/
|
|
143
|
+
fps?: number;
|
|
144
|
+
/** Target average bitrate, in bits/sec. Respected on either implementation. */
|
|
145
|
+
bitrate?: number;
|
|
146
|
+
}
|
|
147
|
+
/** Options for `NativeSimctl.stopVideoRecording`. */
|
|
148
|
+
export interface StopVideoRecordingOptions {
|
|
149
|
+
/**
|
|
150
|
+
* Best-effort: still attempts the native stop, but releases this device's tracked-active-
|
|
151
|
+
* recording bookkeeping regardless of whether that attempt succeeds, instead of leaving it
|
|
152
|
+
* retryable — see the doc comment above `NativeSimctl.stopVideoRecording` for when a caller
|
|
153
|
+
* needs this over a plain retry.
|
|
154
|
+
*/
|
|
155
|
+
force?: boolean;
|
|
122
156
|
}
|
|
123
157
|
/** Options for `NativeSimctl.startVideoStream`. */
|
|
124
158
|
export interface VideoStreamOptions {
|
|
@@ -136,17 +170,29 @@ export interface VideoStreamOptions {
|
|
|
136
170
|
fps?: number;
|
|
137
171
|
/** Target average bitrate, in bits/sec. Defaults to 2,000,000 (2 Mbps). */
|
|
138
172
|
bitrate?: number;
|
|
173
|
+
/**
|
|
174
|
+
* Also stream the device's audio, interleaved into the same `accessUnits()` sequence. Defaults
|
|
175
|
+
* to `false`. Same requirements and failure modes as {@link VideoRecordingOptions.audio} — see
|
|
176
|
+
* its doc comment and the README's "Screen capture" section.
|
|
177
|
+
*/
|
|
178
|
+
audio?: boolean;
|
|
139
179
|
}
|
|
140
180
|
/**
|
|
141
|
-
* One encoded
|
|
142
|
-
* parameter sets
|
|
181
|
+
* One encoded unit from `VideoStream.accessUnits()`, discriminated by `track`: a video unit
|
|
182
|
+
* (Annex-B NAL units — a keyframe's `data` has parameter sets, SPS/PPS or VPS/SPS/PPS for HEVC,
|
|
183
|
+
* prepended, so it's self-decodable alone) or, when {@link VideoStreamOptions.audio} was set, an
|
|
184
|
+
* interleaved audio unit (an ADTS-framed AAC-LC packet — the 7-byte ADTS header carries sample
|
|
185
|
+
* rate/channel count itself, so no separate decoder-config exchange is needed; always
|
|
186
|
+
* independently decodable, so `isKeyFrame` is always `true`). Without `audio`, every unit has
|
|
187
|
+
* `track: 'video'`.
|
|
143
188
|
*/
|
|
144
189
|
export interface VideoAccessUnit {
|
|
190
|
+
track: 'video' | 'audio';
|
|
145
191
|
data: Buffer;
|
|
146
192
|
isKeyFrame: boolean;
|
|
147
|
-
/** Monotonically increasing per
|
|
193
|
+
/** Monotonically increasing per track, starting at 0 — independent between `'video'` and `'audio'`. */
|
|
148
194
|
sequence: number;
|
|
149
|
-
/** Microseconds since the stream started. */
|
|
195
|
+
/** Microseconds since the stream started, on one shared clock across both tracks. */
|
|
150
196
|
timestampMicros: number;
|
|
151
197
|
}
|
|
152
198
|
/**
|
|
@@ -242,6 +288,7 @@ export interface NativeSpawnResult {
|
|
|
242
288
|
export type NativeSpawnExitCallback = (code: number | null, signal: number | null) => void;
|
|
243
289
|
/** Raw shape of an access unit as the native addon delivers it — see {@link VideoAccessUnit}. */
|
|
244
290
|
export interface NativeVideoAccessUnit {
|
|
291
|
+
track: 'video' | 'audio';
|
|
245
292
|
data: Buffer;
|
|
246
293
|
isKeyFrame: boolean;
|
|
247
294
|
sequence: number;
|
|
@@ -249,12 +296,27 @@ export interface NativeVideoAccessUnit {
|
|
|
249
296
|
}
|
|
250
297
|
export type NativeVideoAccessUnitCallback = (unit: NativeVideoAccessUnit) => void;
|
|
251
298
|
export type NativeVideoErrorCallback = (err: Error) => void;
|
|
252
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* A live encoder session, wrapped by `coresim.mm`'s `NativeVideoStream` (video only) or
|
|
301
|
+
* `NativeAVStream` (`audio: true` — see {@link VideoStreamOptions}) — either way, what
|
|
302
|
+
* `NativeDeviceHandle.startVideoStream()` resolves to; the two native wrapper classes expose the
|
|
303
|
+
* identical shape below, so callers never need to know which one they got.
|
|
304
|
+
*/
|
|
253
305
|
export interface NativeVideoStreamHandle {
|
|
254
306
|
stop(): Promise<void>;
|
|
255
|
-
/** Forces the next encoded frame to be a keyframe — trivial in-memory flag, so synchronous. */
|
|
307
|
+
/** Forces the next encoded video frame to be a keyframe — trivial in-memory flag, so synchronous. */
|
|
256
308
|
requestKeyFrame(): void;
|
|
257
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* A live recording, wrapped by `coresim.mm`'s `NativePrivateRecordingHandle` (video only,
|
|
312
|
+
* addressing CoreSimulator's own internally-tracked private recorder) or `NativeAVRecording`
|
|
313
|
+
* (`audio: true` — see {@link VideoRecordingOptions}, a real local resource with no server-side
|
|
314
|
+
* counterpart) — either way, what `NativeDeviceHandle.startVideoRecording()` resolves to.
|
|
315
|
+
*/
|
|
316
|
+
export interface NativeVideoRecordingHandle {
|
|
317
|
+
/** Resolves once the output file has been finalized on disk and is safe to read. */
|
|
318
|
+
stop(): Promise<void>;
|
|
319
|
+
}
|
|
258
320
|
/** A `SimDevice`, wrapped by `coresim.mm`'s `NativeDevice` — what `NativeSimctl`'s `_findDevice()` resolves to. */
|
|
259
321
|
export interface NativeDeviceHandle {
|
|
260
322
|
udid(): string;
|
|
@@ -305,17 +367,20 @@ export interface NativeDeviceHandle {
|
|
|
305
367
|
quality?: number;
|
|
306
368
|
}): Promise<Buffer>;
|
|
307
369
|
getDisplays(): Promise<SimDisplayInfo[]>;
|
|
308
|
-
startVideoRecording(outputFile: string, options
|
|
370
|
+
startVideoRecording(outputFile: string, options: {
|
|
309
371
|
displayId?: string;
|
|
310
372
|
codec?: 'h264' | 'hevc';
|
|
311
373
|
mask?: 'ignored' | 'alpha' | 'black';
|
|
312
|
-
|
|
313
|
-
|
|
374
|
+
audio?: boolean;
|
|
375
|
+
fps?: number;
|
|
376
|
+
bitrate?: number;
|
|
377
|
+
} | undefined, onError: NativeVideoErrorCallback): Promise<NativeVideoRecordingHandle>;
|
|
314
378
|
startVideoStream(options: {
|
|
315
379
|
displayId?: string;
|
|
316
380
|
codec?: 'h264' | 'hevc';
|
|
317
381
|
fps?: number;
|
|
318
382
|
bitrate?: number;
|
|
383
|
+
audio?: boolean;
|
|
319
384
|
} | undefined, onAccessUnit: NativeVideoAccessUnitCallback, onError: NativeVideoErrorCallback): Promise<NativeVideoStreamHandle>;
|
|
320
385
|
spawn(path: string, options: SpawnOptions | undefined, onExit: NativeSpawnExitCallback): Promise<NativeSpawnResult>;
|
|
321
386
|
}
|
|
@@ -336,5 +401,12 @@ export interface NativeServiceContextHandle {
|
|
|
336
401
|
export interface NativeCoreSimModule {
|
|
337
402
|
sharedServiceContext(developerDir: string): Promise<NativeServiceContextHandle>;
|
|
338
403
|
frameworkVersion(): Promise<string>;
|
|
404
|
+
/**
|
|
405
|
+
* Synchronously (not a Promise) stops every still-live video/AV stream or AV recording, blocking
|
|
406
|
+
* until each has released its resources. Meant to be called from a `process.on('exit', ...)`
|
|
407
|
+
* listener (see native-simctl.ts) — cleanup hooks alone don't run under `process.exit()` on the
|
|
408
|
+
* main process/thread, only on a natural empty-event-loop exit or a Worker's own termination.
|
|
409
|
+
*/
|
|
410
|
+
flushActiveSessions(): void;
|
|
339
411
|
}
|
|
340
412
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,oBAAY,cAAc;IACxB,QAAQ,IAAI;IACZ,QAAQ,IAAI;IACZ,OAAO,IAAI;IACX,MAAM,IAAI;IACV,YAAY,IAAI;CACjB;AAED,uGAAuG;AACvG,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,cAAc,CAAC;IACtB,gHAAgH;IAChH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wGAAwG;IACxG,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,+GAA+G;AAC/G,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wGAAwG;AACxG,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,oBAAY,aAAa;IACvB,OAAO,IAAI;IACX,sBAAsB,IAAI;IAC1B,kBAAkB,IAAI;IACtB,uDAAuD;IACvD,QAAQ,aAAa;CACtB;AAED,uGAAuG;AACvG,MAAM,WAAW,WAAW;IAC1B,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAC5B,UAAU,GACV,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,MAAM,GACN,QAAQ,GACR,cAAc,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,sGAAsG;IACtG,EAAE,EAAE,MAAM,CAAC;IACX,sFAAsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
package/lib/src/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAE,YAAY,EAAC,MAAM,mBAAmB,CAAC"}
|
package/lib/src/utils/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAE,YAAY,EAAC,MAAM,mBAAmB,CAAC"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
/** Shared by NativeSimctl and every `src/commands/*.ts` mixin — maps a raw error surfaced by the addon to a typed one. */
|
|
2
2
|
export declare function runCatchingAsync<T>(fn: () => Promise<T>): Promise<T>;
|
|
3
|
+
/** `wrapNativeError` always throws — this just gets its thrown value back as a plain return, for
|
|
4
|
+
* a live callback (e.g. a stream/recording's `onError`) that needs to emit or log a typed error
|
|
5
|
+
* rather than raise it. */
|
|
6
|
+
export declare function toTypedError(err: unknown): Error;
|
|
3
7
|
//# sourceMappingURL=run-catching.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-catching.d.ts","sourceRoot":"","sources":["../../../src/utils/run-catching.ts"],"names":[],"mappings":"AAEA,0HAA0H;AAC1H,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAM1E"}
|
|
1
|
+
{"version":3,"file":"run-catching.d.ts","sourceRoot":"","sources":["../../../src/utils/run-catching.ts"],"names":[],"mappings":"AAEA,0HAA0H;AAC1H,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAM1E;AAED;;2BAE2B;AAC3B,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAMhD"}
|
|
@@ -8,4 +8,15 @@ export async function runCatchingAsync(fn) {
|
|
|
8
8
|
wrapNativeError(err);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
/** `wrapNativeError` always throws — this just gets its thrown value back as a plain return, for
|
|
12
|
+
* a live callback (e.g. a stream/recording's `onError`) that needs to emit or log a typed error
|
|
13
|
+
* rather than raise it. */
|
|
14
|
+
export function toTypedError(err) {
|
|
15
|
+
try {
|
|
16
|
+
wrapNativeError(err);
|
|
17
|
+
}
|
|
18
|
+
catch (wrapped) {
|
|
19
|
+
return wrapped;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
//# sourceMappingURL=run-catching.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-catching.js","sourceRoot":"","sources":["../../../src/utils/run-catching.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,eAAe,EAAC,MAAM,cAAc,CAAC;AAE7C,0HAA0H;AAC1H,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAI,EAAoB;IAC5D,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAC;IACpB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAe,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"run-catching.js","sourceRoot":"","sources":["../../../src/utils/run-catching.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,eAAe,EAAC,MAAM,cAAc,CAAC;AAE7C,0HAA0H;AAC1H,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAI,EAAoB;IAC5D,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAC;IACpB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAe,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED;;2BAE2B;AAC3B,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,IAAI,CAAC;QACH,eAAe,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,OAAO,EAAE,CAAC;QACjB,OAAO,OAAgB,CAAC;IAC1B,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
+
import {logger} from '@appium/support';
|
|
4
|
+
|
|
3
5
|
import type {NativeSimctl} from '../native-simctl.js';
|
|
4
|
-
import type {VideoRecordingOptions} from '../types.js';
|
|
5
|
-
import {runCatchingAsync} from '../utils/index.js';
|
|
6
|
+
import type {NativeVideoRecordingHandle, StopVideoRecordingOptions, VideoRecordingOptions} from '../types.js';
|
|
7
|
+
import {runCatchingAsync, toTypedError} from '../utils/index.js';
|
|
6
8
|
|
|
7
9
|
declare module '../native-simctl.js' {
|
|
8
10
|
interface NativeSimctl {
|
|
9
11
|
startVideoRecording(udid: string, outputFile: string, options?: VideoRecordingOptions): Promise<void>;
|
|
10
|
-
stopVideoRecording(udid: string): Promise<void>;
|
|
12
|
+
stopVideoRecording(udid: string, options?: StopVideoRecordingOptions): Promise<void>;
|
|
11
13
|
isVideoRecording(udid: string): Promise<boolean>;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
const log = logger.getLogger('CoreSim');
|
|
18
|
+
|
|
15
19
|
// One device's recording lifecycle: `start` is the in-flight (or already-settled)
|
|
16
|
-
// startVideoRecording call,
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
+
// startVideoRecording call, resolving to the live native handle — with both `options.audio` and
|
|
21
|
+
// `options.fps` unset, this addresses CoreSimulator's own internally-tracked private recorder (no
|
|
22
|
+
// real local resource, same as before this field existed); with either set, it's a real local
|
|
23
|
+
// resource (this addon's own encoders) with no server-side counterpart, so a handle is the only
|
|
24
|
+
// way to stop it either way. `stop`, once set, is the in-flight native stop call shared by every
|
|
25
|
+
// concurrent stopVideoRecording for this same recording — see stopVideoRecording for why a second
|
|
26
|
+
// one must reuse it rather than issuing its own.
|
|
20
27
|
interface RecordingState {
|
|
21
|
-
readonly start: Promise<
|
|
28
|
+
readonly start: Promise<NativeVideoRecordingHandle>;
|
|
22
29
|
stop?: Promise<void>;
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -27,17 +34,24 @@ interface RecordingState {
|
|
|
27
34
|
const activeRecordings = new Map<string, RecordingState>();
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
|
-
* Starts recording the device's display
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
37
|
+
* Starts recording the device's display (and, with `options.audio`, its audio too, muxed as a
|
|
38
|
+
* second track) to `outputFile` — the native equivalent of `simctl io <udid> recordVideo` when
|
|
39
|
+
* both `audio` and `fps` are unset. Resolves once the first frame has actually been recorded, so
|
|
40
|
+
* it's always safe to call {@link stopVideoRecording} immediately after. Only one recording may be
|
|
41
|
+
* active per device at a time; starting a second one while the first is still running rejects.
|
|
42
|
+
*
|
|
43
|
+
* Without `audio`/`fps`, rejects with `NativeSimUnavailableError` if this CoreSimulator predates
|
|
44
|
+
* the private capture API — confirmed missing on Xcode 16.4's, present on Xcode 26.5+ (Apple
|
|
45
|
+
* documents no exact version floor). With either set, see {@link VideoRecordingOptions.audio}'s
|
|
46
|
+
* own doc comment for the host-permission requirement that applies specifically to `audio`; a
|
|
47
|
+
* failure *after* this call has already resolved (e.g. the audio tap's guest process set vanishing
|
|
48
|
+
* mid-recording) doesn't reject it — it's logged here instead — call {@link stopVideoRecording} to
|
|
49
|
+
* observe it as a rejection and release the recording's resources.
|
|
36
50
|
*
|
|
37
51
|
* @param udid — UDID of the device to record; must be booted
|
|
38
52
|
* @param outputFile — filesystem path to write the video to; resolved against `process.cwd()` if
|
|
39
53
|
* relative, since the native layer requires an absolute path
|
|
40
|
-
* @param options — `displayId`, `codec`, `mask` — see {@link VideoRecordingOptions}
|
|
54
|
+
* @param options — `displayId`, `codec`, `mask`, `audio`, `fps`, `bitrate` — see {@link VideoRecordingOptions}
|
|
41
55
|
* @throws {Error} if a recording is already in progress for this device
|
|
42
56
|
*/
|
|
43
57
|
export async function startVideoRecording(
|
|
@@ -46,13 +60,28 @@ export async function startVideoRecording(
|
|
|
46
60
|
outputFile: string,
|
|
47
61
|
options: VideoRecordingOptions = {},
|
|
48
62
|
): Promise<void> {
|
|
63
|
+
// Mirrors startVideoStream's identical validation — both reach the same native encoder path,
|
|
64
|
+
// which otherwise silently truncates an out-of-range value instead of rejecting it.
|
|
65
|
+
if (options.fps !== undefined && (!Number.isFinite(options.fps) || options.fps < 1)) {
|
|
66
|
+
throw new RangeError(`fps must be a finite number >= 1, got ${options.fps}`);
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
options.bitrate !== undefined &&
|
|
70
|
+
(!Number.isFinite(options.bitrate) || options.bitrate <= 0 || options.bitrate > 2 ** 31 - 1)
|
|
71
|
+
) {
|
|
72
|
+
throw new RangeError(`bitrate must be a positive number no greater than ${2 ** 31 - 1}, got ${options.bitrate}`);
|
|
73
|
+
}
|
|
49
74
|
const key = udid.toLowerCase();
|
|
50
75
|
if (activeRecordings.has(key)) {
|
|
51
76
|
throw new Error(`A video recording is already in progress for device '${udid}'`);
|
|
52
77
|
}
|
|
53
78
|
const absoluteOutputFile = path.resolve(outputFile);
|
|
54
79
|
const startPromise = runCatchingAsync(async () =>
|
|
55
|
-
(await this._findDevice(udid)).startVideoRecording(absoluteOutputFile, options)
|
|
80
|
+
(await this._findDevice(udid)).startVideoRecording(absoluteOutputFile, options, (err) => {
|
|
81
|
+
// Only ever invoked on the `audio`/`fps` (own-encoder) path — see its own doc comment above.
|
|
82
|
+
// No live consumer to report to (this call already resolved), so logged instead of lost.
|
|
83
|
+
log.error(`Unhandled video recording error for device '${udid}': ${toTypedError(err).stack ?? err}`);
|
|
84
|
+
}),
|
|
56
85
|
);
|
|
57
86
|
// Marked before the native call resolves, not after, so a concurrent startVideoRecording for the
|
|
58
87
|
// same device is rejected immediately instead of racing this one — rolled back below on failure.
|
|
@@ -83,34 +112,55 @@ export async function startVideoRecording(
|
|
|
83
112
|
* device-lookup error — leaves the recording tracked as still active so a retry can reach the
|
|
84
113
|
* native call rather than the caller losing the ability to stop it at all.
|
|
85
114
|
*
|
|
115
|
+
* That retryability is a liability if nothing ever *does* retry — e.g. a best-effort teardown path
|
|
116
|
+
* that must not throw, so it logs a failed stop and moves on — since without `options.force` there
|
|
117
|
+
* is no other way to release the entry, permanently blocking a new `startVideoRecording` for this
|
|
118
|
+
* device. Pass `options.force: true` in that situation: it still attempts the native stop (so the
|
|
119
|
+
* encoder/file gets a chance to finalize cleanly) but releases the entry unconditionally afterward
|
|
120
|
+
* and never rejects, even if that attempt failed — understand that this can leave a native
|
|
121
|
+
* resource dangling if the stop genuinely never lands.
|
|
122
|
+
*
|
|
86
123
|
* @param udid — UDID of the device to stop recording
|
|
124
|
+
* @param options — `force` (see above)
|
|
87
125
|
* @throws {Error} if no recording is currently in progress for this device
|
|
88
126
|
*/
|
|
89
|
-
export async function stopVideoRecording(
|
|
127
|
+
export async function stopVideoRecording(
|
|
128
|
+
this: NativeSimctl,
|
|
129
|
+
udid: string,
|
|
130
|
+
options: StopVideoRecordingOptions = {},
|
|
131
|
+
): Promise<void> {
|
|
90
132
|
const key = udid.toLowerCase();
|
|
91
133
|
const state = activeRecordings.get(key);
|
|
92
134
|
if (!state) {
|
|
93
135
|
throw new Error(`No video recording is in progress for device '${udid}'`);
|
|
94
136
|
}
|
|
137
|
+
let handle: NativeVideoRecordingHandle;
|
|
95
138
|
try {
|
|
96
|
-
await state.start;
|
|
139
|
+
handle = await state.start;
|
|
97
140
|
} catch {
|
|
98
141
|
// start itself failed — its own catch already cleaned up activeRecordings; nothing to stop.
|
|
99
142
|
throw new Error(`No video recording is in progress for device '${udid}'`);
|
|
100
143
|
}
|
|
101
|
-
const stopPromise = (state.stop ??= runCatchingAsync(async () =>
|
|
102
|
-
|
|
103
|
-
));
|
|
144
|
+
const stopPromise = (state.stop ??= runCatchingAsync(async () => {
|
|
145
|
+
await handle.stop();
|
|
146
|
+
}));
|
|
104
147
|
try {
|
|
105
148
|
await stopPromise;
|
|
106
149
|
} catch (e) {
|
|
107
|
-
// Only clear our own attempt, and only if it's still the current one — a future
|
|
108
|
-
// able to retry the native stop rather than replaying this same rejection
|
|
109
|
-
// not clobber a newer attempt another concurrent caller may have already
|
|
150
|
+
// Only clear our own attempt, and only if it's still the current one — a future (non-forced)
|
|
151
|
+
// call must be able to retry the native stop rather than replaying this same rejection
|
|
152
|
+
// forever, but must not clobber a newer attempt another concurrent caller may have already
|
|
153
|
+
// started instead.
|
|
110
154
|
if (state.stop === stopPromise) {
|
|
111
155
|
state.stop = undefined;
|
|
112
156
|
}
|
|
113
|
-
|
|
157
|
+
if (!options.force) {
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
log.warn(
|
|
161
|
+
`Force-releasing video recording bookkeeping for device '${udid}' despite a failed native stop: ` +
|
|
162
|
+
`${toTypedError(e).stack ?? e}`,
|
|
163
|
+
);
|
|
114
164
|
}
|
|
115
165
|
if (activeRecordings.get(key) === state) {
|
|
116
166
|
activeRecordings.delete(key);
|
|
@@ -2,10 +2,9 @@ import {EventEmitter} from 'node:events';
|
|
|
2
2
|
|
|
3
3
|
import {logger} from '@appium/support';
|
|
4
4
|
|
|
5
|
-
import {wrapNativeError} from '../errors.js';
|
|
6
5
|
import type {NativeSimctl} from '../native-simctl.js';
|
|
7
6
|
import type {NativeVideoStreamHandle, VideoAccessUnit, VideoStreamOptions} from '../types.js';
|
|
8
|
-
import {runCatchingAsync} from '../utils/index.js';
|
|
7
|
+
import {runCatchingAsync, toTypedError} from '../utils/index.js';
|
|
9
8
|
|
|
10
9
|
declare module '../native-simctl.js' {
|
|
11
10
|
interface NativeSimctl {
|
|
@@ -20,55 +19,57 @@ const log = logger.getLogger('CoreSim');
|
|
|
20
19
|
// backpressure: _handleAccessUnit's emit-equivalent push always returns immediately, regardless of
|
|
21
20
|
// how slow the actual accessUnits() consumer is, so without a bound of its own this queue could
|
|
22
21
|
// otherwise grow without limit while a slow consumer falls behind.
|
|
23
|
-
const MAX_BUFFERED_UNITS = 60;
|
|
22
|
+
export const MAX_BUFFERED_UNITS = 60;
|
|
24
23
|
|
|
25
24
|
const SINGLE_CONSUMER_ERROR =
|
|
26
25
|
'VideoStream.accessUnits() supports only one active consumer at a time — a second concurrent call rejects.';
|
|
27
26
|
|
|
28
|
-
/** `wrapNativeError` always throws — this just gets its thrown value back as a plain return, to emit rather than raise it. */
|
|
29
|
-
function toTypedError(err: unknown): Error {
|
|
30
|
-
try {
|
|
31
|
-
wrapNativeError(err);
|
|
32
|
-
} catch (wrapped) {
|
|
33
|
-
return wrapped as Error;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
/**
|
|
38
28
|
* Single-consumer FIFO between native's per-frame callback and `accessUnits()`. Unlike routing
|
|
39
29
|
* through `EventEmitter`, a unit pushed before any consumer has started iterating is retained
|
|
40
30
|
* (fixing the encoder's own first-frame/keyframe otherwise being lost to a startup race) rather
|
|
41
31
|
* than silently dropped.
|
|
42
32
|
*
|
|
43
|
-
* Bounded at `MAX_BUFFERED_UNITS`, but
|
|
44
|
-
* falls behind: since
|
|
45
|
-
* `AllowFrameReordering`), dropping an arbitrary
|
|
33
|
+
* Bounded at `MAX_BUFFERED_UNITS`, but resync-aware about *how* it sheds load once a slow consumer
|
|
34
|
+
* falls behind: since interframes only reference earlier frames they were encoded against (no
|
|
35
|
+
* `AllowFrameReordering`), dropping an arbitrary one would orphan every later one from its
|
|
46
36
|
* reference chain, corrupting decode from that point on even though delivery looks unbroken.
|
|
47
|
-
* Overflow instead clears the backlog entirely and enters a resync state
|
|
48
|
-
* further
|
|
49
|
-
*
|
|
37
|
+
* Overflow instead clears the backlog entirely and enters a resync state — `dropsDuringResync`
|
|
38
|
+
* decides which further units to discard (not buffer) until `endsResync` sees a self-decodable
|
|
39
|
+
* point to resume clean delivery from. Generic (not hardcoded to video's own reference-chain
|
|
40
|
+
* concern) so `VideoStream` can reuse this same machinery when it's carrying interleaved audio
|
|
41
|
+
* too (`VideoStreamOptions.audio`) — an audio unit is always independently decodable, so it's
|
|
42
|
+
* configured to never be dropped and never itself end a resync; only video interframes are.
|
|
50
43
|
*/
|
|
51
|
-
class AccessUnitQueue {
|
|
52
|
-
private readonly buffer:
|
|
53
|
-
private waiter:
|
|
54
|
-
| {resolve: (result: IteratorResult<VideoAccessUnit>) => void; reject: (err: unknown) => void}
|
|
55
|
-
| undefined;
|
|
44
|
+
export class AccessUnitQueue<T> {
|
|
45
|
+
private readonly buffer: T[] = [];
|
|
46
|
+
private waiter: {resolve: (result: IteratorResult<T>) => void; reject: (err: unknown) => void} | undefined;
|
|
56
47
|
private ended = false;
|
|
57
48
|
private error: unknown;
|
|
58
49
|
private resyncing = false;
|
|
59
50
|
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly opts: {
|
|
53
|
+
/** Whether `unit` should be discarded (not buffered) while resyncing. */
|
|
54
|
+
dropsDuringResync: (unit: T) => boolean;
|
|
55
|
+
/** Whether `unit` is a self-decodable point resync can resume clean delivery from. */
|
|
56
|
+
endsResync: (unit: T) => boolean;
|
|
57
|
+
/** Called once when overflow first forces a resync, e.g. to request a fresh keyframe. */
|
|
58
|
+
onOverflow?: () => void;
|
|
59
|
+
},
|
|
60
|
+
) {}
|
|
62
61
|
|
|
63
|
-
push(unit:
|
|
62
|
+
push(unit: T): void {
|
|
64
63
|
if (this.ended) {
|
|
65
64
|
return;
|
|
66
65
|
}
|
|
67
66
|
if (this.resyncing) {
|
|
68
|
-
if (
|
|
67
|
+
if (this.opts.dropsDuringResync(unit)) {
|
|
69
68
|
return; // still waiting for a self-decodable point to resume delivery from
|
|
70
69
|
}
|
|
71
|
-
this.
|
|
70
|
+
if (this.opts.endsResync(unit)) {
|
|
71
|
+
this.resyncing = false;
|
|
72
|
+
}
|
|
72
73
|
}
|
|
73
74
|
if (this.waiter) {
|
|
74
75
|
const {resolve} = this.waiter;
|
|
@@ -80,7 +81,7 @@ class AccessUnitQueue {
|
|
|
80
81
|
if (this.buffer.length > MAX_BUFFERED_UNITS) {
|
|
81
82
|
this.buffer.length = 0;
|
|
82
83
|
this.resyncing = true;
|
|
83
|
-
this.onOverflow?.();
|
|
84
|
+
this.opts.onOverflow?.();
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -119,7 +120,7 @@ class AccessUnitQueue {
|
|
|
119
120
|
* queue never hands out a stale buffered unit — an aborted consumer, or a fresh iterator started
|
|
120
121
|
* after `stop()`, must see the boundary immediately rather than draining leftovers first.
|
|
121
122
|
*/
|
|
122
|
-
next(signal: AbortSignal): Promise<IteratorResult<
|
|
123
|
+
next(signal: AbortSignal): Promise<IteratorResult<T>> {
|
|
123
124
|
if (signal.aborted) {
|
|
124
125
|
return Promise.resolve({value: undefined, done: true});
|
|
125
126
|
}
|
|
@@ -151,8 +152,9 @@ class AccessUnitQueue {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
|
-
* A live video stream from `NativeSimctl.startVideoStream` — encodes the device's display
|
|
155
|
-
*
|
|
155
|
+
* A live video stream from `NativeSimctl.startVideoStream` — encodes the device's display (and,
|
|
156
|
+
* with `options.audio`, its audio too — see {@link VideoStreamOptions}) in real time via
|
|
157
|
+
* VideoToolbox/Core Audio, unlike `startVideoRecording`, which drives CoreSimulator's own private,
|
|
156
158
|
* file-only recorder. Mirrors `appium-ios-remotexpc`'s `ScreenStreamCapture` shape
|
|
157
159
|
* (`accessUnits()`/`stop()`) for API consistency; the transport is otherwise unrelated.
|
|
158
160
|
*/
|
|
@@ -162,8 +164,14 @@ export class VideoStream extends EventEmitter {
|
|
|
162
164
|
private stopPromise: Promise<void> | undefined;
|
|
163
165
|
// Requests a fresh keyframe on resync so delivery can resume immediately rather than waiting
|
|
164
166
|
// for the next periodic one — `handle` may not be attached yet on a startup-time overflow
|
|
165
|
-
// (vanishingly unlikely given MAX_BUFFERED_UNITS), in which case this is just a no-op.
|
|
166
|
-
|
|
167
|
+
// (vanishingly unlikely given MAX_BUFFERED_UNITS), in which case this is just a no-op. Only a
|
|
168
|
+
// video keyframe ends/is exempt from resync — an audio unit (when present) is always
|
|
169
|
+
// independently decodable, so it's never dropped and never itself ends a resync.
|
|
170
|
+
private readonly queue = new AccessUnitQueue<VideoAccessUnit>({
|
|
171
|
+
dropsDuringResync: (unit) => unit.track === 'video' && !unit.isKeyFrame,
|
|
172
|
+
endsResync: (unit) => unit.track === 'video' && unit.isKeyFrame,
|
|
173
|
+
onOverflow: () => this.handle?.requestKeyFrame(),
|
|
174
|
+
});
|
|
167
175
|
private activeConsumers = 0;
|
|
168
176
|
|
|
169
177
|
/** @internal */
|
|
@@ -236,13 +244,14 @@ export class VideoStream extends EventEmitter {
|
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
/**
|
|
239
|
-
* Starts encoding the device's display
|
|
240
|
-
* started; the returned {@link VideoStream}'s
|
|
241
|
-
*
|
|
242
|
-
*
|
|
247
|
+
* Starts encoding the device's display (and, with `options.audio`, its audio) in real time.
|
|
248
|
+
* Resolves once the encoder(s) have actually started; the returned {@link VideoStream}'s
|
|
249
|
+
* `accessUnits()` then yields each unit as it arrives. Independent of
|
|
250
|
+
* `startVideoRecording`/`stopVideoRecording` — both, and any number of concurrent streams, can
|
|
251
|
+
* run on the same device at once.
|
|
243
252
|
*
|
|
244
253
|
* @param udid — UDID of the device to stream; must be booted
|
|
245
|
-
* @param options — `displayId`, `codec`, `fps`, `bitrate` — see {@link VideoStreamOptions}
|
|
254
|
+
* @param options — `displayId`, `codec`, `fps`, `bitrate`, `audio` — see {@link VideoStreamOptions}
|
|
246
255
|
*/
|
|
247
256
|
export async function startVideoStream(
|
|
248
257
|
this: NativeSimctl,
|