@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/src/coresim.mm
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
#include <vector>
|
|
26
26
|
|
|
27
27
|
#include "native/async_bridge.h"
|
|
28
|
+
#include "native/av_recording.h"
|
|
29
|
+
#include "native/av_stream.h"
|
|
28
30
|
#include "native/nserror_bridge.h"
|
|
29
31
|
#include "native/objc_runtime.h"
|
|
30
32
|
#include "native/sim_device.h"
|
|
@@ -182,6 +184,29 @@ struct RuntimeEntry {
|
|
|
182
184
|
std::string versionString;
|
|
183
185
|
};
|
|
184
186
|
|
|
187
|
+
// Napi::ThreadSafeFunction::Release() and Abort() are two mutually exclusive modes of one
|
|
188
|
+
// underlying napi_release_threadsafe_function call — Node's own docs say calling either a second
|
|
189
|
+
// time (including calling the other one after the first) is undefined behavior, since the handle
|
|
190
|
+
// may already be destroyed. A stream's normal stop-triggered release (onEnd) and
|
|
191
|
+
// CleanupActiveSessions's exit-time AbortDelivery can race — a session stays registered until its
|
|
192
|
+
// JS wrapper is GC'd, not until Stop() completes, so an already-stopped-but-still-referenced
|
|
193
|
+
// stream can still be hit by AbortDelivery() later. This guard makes whichever of Release()/
|
|
194
|
+
// Abort() runs first win, and turns the other into a no-op instead of a second, unsafe call.
|
|
195
|
+
struct TsfnReleaseGuard {
|
|
196
|
+
std::mutex mutex;
|
|
197
|
+
bool done = false;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
template <typename Fn>
|
|
201
|
+
void ReleaseTsfnOnce(const std::shared_ptr<TsfnReleaseGuard>& guard, Fn&& releaseOrAbort) {
|
|
202
|
+
std::lock_guard<std::mutex> lock(guard->mutex);
|
|
203
|
+
if (guard->done) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
guard->done = true;
|
|
207
|
+
releaseOrAbort();
|
|
208
|
+
}
|
|
209
|
+
|
|
185
210
|
// Node-API explicitly prohibits sharing an Environment's data across Environments (e.g. two
|
|
186
211
|
// worker_threads instances each `require()`-ing this addon) — each gets its own separate call
|
|
187
212
|
// into Init() below. A process-global `static Napi::FunctionReference` per class would let one
|
|
@@ -190,45 +215,54 @@ struct RuntimeEntry {
|
|
|
190
215
|
// Napi::Env::SetInstanceData/GetInstanceData instead keeps each Environment's constructors
|
|
191
216
|
// scoped to it, and cleans them up automatically (the default finalizer just `delete`s this) when
|
|
192
217
|
// that Environment tears down.
|
|
218
|
+
// A registry of live sessions (VideoStreamSession/AVStreamSession/AVRecordingSession) of one
|
|
219
|
+
// type, so the env cleanup hook can stop every still-live one — and thus release its
|
|
220
|
+
// ThreadSafeFunctions — before Node force-tears-down this Environment's own TSFNs (see Init's
|
|
221
|
+
// AddCleanupHook calls for why this ordering matters). `T` must expose a blocking `Stop()`.
|
|
222
|
+
template <typename T>
|
|
223
|
+
struct ActiveSessionRegistry {
|
|
224
|
+
std::mutex mutex;
|
|
225
|
+
std::vector<std::shared_ptr<T>> sessions;
|
|
226
|
+
|
|
227
|
+
void Register(const std::shared_ptr<T>& session) {
|
|
228
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
229
|
+
sessions.push_back(session);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
void Deregister(const std::shared_ptr<T>& session) {
|
|
233
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
234
|
+
sessions.erase(std::remove(sessions.begin(), sessions.end(), session), sessions.end());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Stops every still-registered session, blocking until each has fully torn down. `stop` invokes
|
|
238
|
+
// each session's own Stop() — a parameter (rather than always calling `Stop()` with no
|
|
239
|
+
// arguments) since AVRecordingSession's Stop() takes a completion callback the others don't.
|
|
240
|
+
template <typename StopFn>
|
|
241
|
+
void StopAll(StopFn stop) {
|
|
242
|
+
std::vector<std::shared_ptr<T>> toStop;
|
|
243
|
+
{
|
|
244
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
245
|
+
toStop.swap(sessions);
|
|
246
|
+
}
|
|
247
|
+
for (auto& session : toStop) {
|
|
248
|
+
stop(session);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
193
253
|
struct AddonInstanceData {
|
|
194
254
|
Napi::FunctionReference deviceConstructor;
|
|
195
255
|
Napi::FunctionReference deviceSetConstructor;
|
|
196
256
|
Napi::FunctionReference serviceContextConstructor;
|
|
197
257
|
Napi::FunctionReference videoStreamConstructor;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
258
|
+
Napi::FunctionReference avStreamConstructor;
|
|
259
|
+
Napi::FunctionReference avRecordingConstructor;
|
|
260
|
+
Napi::FunctionReference privateRecordingConstructor;
|
|
261
|
+
ActiveSessionRegistry<coresim::VideoStreamSession> activeVideoStreams;
|
|
262
|
+
ActiveSessionRegistry<coresim::AVStreamSession> activeAVStreams;
|
|
263
|
+
ActiveSessionRegistry<coresim::AVRecordingSession> activeAVRecordings;
|
|
203
264
|
};
|
|
204
265
|
|
|
205
|
-
void RegisterActiveStream(Napi::Env env, const std::shared_ptr<coresim::VideoStreamSession>& session) {
|
|
206
|
-
auto* instanceData = env.GetInstanceData<AddonInstanceData>();
|
|
207
|
-
std::lock_guard<std::mutex> lock(instanceData->activeVideoStreamsMutex);
|
|
208
|
-
instanceData->activeVideoStreams.push_back(session);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
void DeregisterActiveStream(Napi::Env env, const std::shared_ptr<coresim::VideoStreamSession>& session) {
|
|
212
|
-
auto* instanceData = env.GetInstanceData<AddonInstanceData>();
|
|
213
|
-
std::lock_guard<std::mutex> lock(instanceData->activeVideoStreamsMutex);
|
|
214
|
-
auto& streams = instanceData->activeVideoStreams;
|
|
215
|
-
streams.erase(std::remove(streams.begin(), streams.end(), session), streams.end());
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Stops every still-registered stream, blocking until each has fully torn down (including its own
|
|
219
|
-
// onEnd_ releasing its ThreadSafeFunctions) — see the env cleanup hook this backs, in Init below,
|
|
220
|
-
// for why this must run to completion before returning.
|
|
221
|
-
void StopAllActiveStreams(AddonInstanceData* instanceData) {
|
|
222
|
-
std::vector<std::shared_ptr<coresim::VideoStreamSession>> streams;
|
|
223
|
-
{
|
|
224
|
-
std::lock_guard<std::mutex> lock(instanceData->activeVideoStreamsMutex);
|
|
225
|
-
streams.swap(instanceData->activeVideoStreams);
|
|
226
|
-
}
|
|
227
|
-
for (auto& session : streams) {
|
|
228
|
-
session->Stop();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
266
|
} // namespace
|
|
233
267
|
|
|
234
268
|
// Wraps a live coresim::VideoStreamSession. Access units/errors are delivered live via the
|
|
@@ -262,7 +296,7 @@ class NativeVideoStream : public Napi::ObjectWrap<NativeVideoStream> {
|
|
|
262
296
|
void Finalize(Napi::Env env) override {
|
|
263
297
|
auto session = std::move(session_);
|
|
264
298
|
if (session) {
|
|
265
|
-
|
|
299
|
+
env.GetInstanceData<AddonInstanceData>()->activeVideoStreams.Deregister(session);
|
|
266
300
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
267
301
|
session->Stop();
|
|
268
302
|
});
|
|
@@ -273,7 +307,7 @@ class NativeVideoStream : public Napi::ObjectWrap<NativeVideoStream> {
|
|
|
273
307
|
NativeVideoStream::NativeVideoStream(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeVideoStream>(info) {
|
|
274
308
|
auto* boxed = info[0].As<Napi::External<std::shared_ptr<coresim::VideoStreamSession>>>().Data();
|
|
275
309
|
session_ = *boxed;
|
|
276
|
-
|
|
310
|
+
info.Env().GetInstanceData<AddonInstanceData>()->activeVideoStreams.Register(session_);
|
|
277
311
|
}
|
|
278
312
|
|
|
279
313
|
void NativeVideoStream::Init(Napi::Env env) {
|
|
@@ -292,6 +326,189 @@ Napi::Object NativeVideoStream::NewInstance(Napi::Env env, std::shared_ptr<cores
|
|
|
292
326
|
env, boxed, [](Napi::Env /*env*/, std::shared_ptr<coresim::VideoStreamSession>* data) { delete data; })});
|
|
293
327
|
}
|
|
294
328
|
|
|
329
|
+
// Wraps a live coresim::AVStreamSession — the combined-AV counterpart to NativeVideoStream above,
|
|
330
|
+
// same shape (only `stop()`/`requestKeyFrame()`; access units/errors are delivered live via the
|
|
331
|
+
// callbacks passed directly to startAVStream).
|
|
332
|
+
class NativeAVStream : public Napi::ObjectWrap<NativeAVStream> {
|
|
333
|
+
public:
|
|
334
|
+
static void Init(Napi::Env env);
|
|
335
|
+
static Napi::Object NewInstance(Napi::Env env, std::shared_ptr<coresim::AVStreamSession> session);
|
|
336
|
+
explicit NativeAVStream(const Napi::CallbackInfo& info);
|
|
337
|
+
|
|
338
|
+
private:
|
|
339
|
+
std::shared_ptr<coresim::AVStreamSession> session_;
|
|
340
|
+
|
|
341
|
+
Napi::Value Stop(const Napi::CallbackInfo& info) {
|
|
342
|
+
Napi::Env env = info.Env();
|
|
343
|
+
auto session = session_;
|
|
344
|
+
return RunAsyncVoid(env, [session]() { session->Stop(); });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
Napi::Value RequestKeyFrame(const Napi::CallbackInfo& info) {
|
|
348
|
+
if (session_) {
|
|
349
|
+
session_->RequestKeyFrame();
|
|
350
|
+
}
|
|
351
|
+
return info.Env().Undefined();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// See NativeVideoStream::Finalize for why this hands off to a background queue.
|
|
355
|
+
void Finalize(Napi::Env env) override {
|
|
356
|
+
auto session = std::move(session_);
|
|
357
|
+
if (session) {
|
|
358
|
+
env.GetInstanceData<AddonInstanceData>()->activeAVStreams.Deregister(session);
|
|
359
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
360
|
+
session->Stop();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
NativeAVStream::NativeAVStream(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeAVStream>(info) {
|
|
367
|
+
auto* boxed = info[0].As<Napi::External<std::shared_ptr<coresim::AVStreamSession>>>().Data();
|
|
368
|
+
session_ = *boxed;
|
|
369
|
+
info.Env().GetInstanceData<AddonInstanceData>()->activeAVStreams.Register(session_);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
void NativeAVStream::Init(Napi::Env env) {
|
|
373
|
+
Napi::Function ctor = DefineClass(env, "NativeAVStream",
|
|
374
|
+
{
|
|
375
|
+
InstanceMethod<&NativeAVStream::Stop>("stop"),
|
|
376
|
+
InstanceMethod<&NativeAVStream::RequestKeyFrame>("requestKeyFrame"),
|
|
377
|
+
});
|
|
378
|
+
env.GetInstanceData<AddonInstanceData>()->avStreamConstructor = Napi::Persistent(ctor);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
Napi::Object NativeAVStream::NewInstance(Napi::Env env, std::shared_ptr<coresim::AVStreamSession> session) {
|
|
382
|
+
auto* boxed = new std::shared_ptr<coresim::AVStreamSession>(std::move(session));
|
|
383
|
+
Napi::Function ctor = env.GetInstanceData<AddonInstanceData>()->avStreamConstructor.Value();
|
|
384
|
+
return ctor.New({Napi::External<std::shared_ptr<coresim::AVStreamSession>>::New(
|
|
385
|
+
env, boxed, [](Napi::Env /*env*/, std::shared_ptr<coresim::AVStreamSession>* data) { delete data; })});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Wraps a live coresim::AVRecordingSession, kept alive between startAVRecording and its returned
|
|
389
|
+
// handle's stop() — unlike startVideoRecording/stopVideoRecording, which address CoreSimulator's
|
|
390
|
+
// own internally-tracked private recorder purely by udid, this session is a real local resource
|
|
391
|
+
// (VTCompressionSession + Core Audio tap + AVAssetWriter) with no equivalent server-side handle.
|
|
392
|
+
class NativeAVRecording : public Napi::ObjectWrap<NativeAVRecording> {
|
|
393
|
+
public:
|
|
394
|
+
static void Init(Napi::Env env);
|
|
395
|
+
static Napi::Object NewInstance(Napi::Env env, std::shared_ptr<coresim::AVRecordingSession> session);
|
|
396
|
+
explicit NativeAVRecording(const Napi::CallbackInfo& info);
|
|
397
|
+
|
|
398
|
+
private:
|
|
399
|
+
std::shared_ptr<coresim::AVRecordingSession> session_;
|
|
400
|
+
|
|
401
|
+
// Resolves once the output file has been finalized on disk and is safe to read — mirrors
|
|
402
|
+
// stopVideoRecording's own contract.
|
|
403
|
+
Napi::Value Stop(const Napi::CallbackInfo& info) {
|
|
404
|
+
Napi::Env env = info.Env();
|
|
405
|
+
auto session = session_;
|
|
406
|
+
return RunAsyncVoid(env, [session]() {
|
|
407
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
408
|
+
NSError* capturedError = nil;
|
|
409
|
+
session->Stop([sema, &capturedError](NSError* error) {
|
|
410
|
+
capturedError = error;
|
|
411
|
+
dispatch_semaphore_signal(sema);
|
|
412
|
+
});
|
|
413
|
+
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
414
|
+
if (capturedError != nil) {
|
|
415
|
+
throw NSErrorException(capturedError);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// See NativeVideoStream::Finalize for why this hands off to a background queue — Stop()'s own
|
|
421
|
+
// completion handler here is simply dropped rather than awaited, matching Finalize's existing
|
|
422
|
+
// "GC thread must never block" contract for every session type.
|
|
423
|
+
void Finalize(Napi::Env env) override {
|
|
424
|
+
auto session = std::move(session_);
|
|
425
|
+
if (session) {
|
|
426
|
+
env.GetInstanceData<AddonInstanceData>()->activeAVRecordings.Deregister(session);
|
|
427
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
428
|
+
session->Stop([](NSError*) {});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
NativeAVRecording::NativeAVRecording(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeAVRecording>(info) {
|
|
435
|
+
auto* boxed = info[0].As<Napi::External<std::shared_ptr<coresim::AVRecordingSession>>>().Data();
|
|
436
|
+
session_ = *boxed;
|
|
437
|
+
info.Env().GetInstanceData<AddonInstanceData>()->activeAVRecordings.Register(session_);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
void NativeAVRecording::Init(Napi::Env env) {
|
|
441
|
+
Napi::Function ctor = DefineClass(env, "NativeAVRecording",
|
|
442
|
+
{
|
|
443
|
+
InstanceMethod<&NativeAVRecording::Stop>("stop"),
|
|
444
|
+
});
|
|
445
|
+
env.GetInstanceData<AddonInstanceData>()->avRecordingConstructor = Napi::Persistent(ctor);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
Napi::Object NativeAVRecording::NewInstance(Napi::Env env, std::shared_ptr<coresim::AVRecordingSession> session) {
|
|
449
|
+
auto* boxed = new std::shared_ptr<coresim::AVRecordingSession>(std::move(session));
|
|
450
|
+
Napi::Function ctor = env.GetInstanceData<AddonInstanceData>()->avRecordingConstructor.Value();
|
|
451
|
+
return ctor.New({Napi::External<std::shared_ptr<coresim::AVRecordingSession>>::New(
|
|
452
|
+
env, boxed, [](Napi::Env /*env*/, std::shared_ptr<coresim::AVRecordingSession>* data) { delete data; })});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Wraps a recording started via CoreSimulator's own private recorder (sim_video_recording.h,
|
|
456
|
+
// addressed purely by `device` — no client-side live resource, unlike AVRecordingSession above)
|
|
457
|
+
// in the same `stop()` shape NativeAVRecording exposes for the `audio: true` path, so
|
|
458
|
+
// NativeDevice::StartVideoRecording can return one handle type either way regardless of which
|
|
459
|
+
// path it took. No registry/cleanup-hook entry needed: CoreSimulator owns the actual recorder
|
|
460
|
+
// state internally, so there's nothing here to force-stop if the JS wrapper is just GC'd.
|
|
461
|
+
class NativePrivateRecordingHandle : public Napi::ObjectWrap<NativePrivateRecordingHandle> {
|
|
462
|
+
public:
|
|
463
|
+
static void Init(Napi::Env env);
|
|
464
|
+
static Napi::Object NewInstance(Napi::Env env, id device);
|
|
465
|
+
explicit NativePrivateRecordingHandle(const Napi::CallbackInfo& info);
|
|
466
|
+
|
|
467
|
+
private:
|
|
468
|
+
id device_;
|
|
469
|
+
|
|
470
|
+
// Mirrors the previous standalone StopVideoRecording native method.
|
|
471
|
+
Napi::Value Stop(const Napi::CallbackInfo& info) {
|
|
472
|
+
Napi::Env env = info.Env();
|
|
473
|
+
id device = device_;
|
|
474
|
+
return RunAsyncVoid(env, [device]() {
|
|
475
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
476
|
+
dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.stopRecordVideo", DISPATCH_QUEUE_SERIAL);
|
|
477
|
+
__block NSError* capturedError = nil;
|
|
478
|
+
NSError* resolveError = nil;
|
|
479
|
+
BOOL ok = coresim::StopVideoRecording(
|
|
480
|
+
device, queue,
|
|
481
|
+
^(NSError* asyncError) {
|
|
482
|
+
capturedError = asyncError;
|
|
483
|
+
dispatch_semaphore_signal(sema);
|
|
484
|
+
},
|
|
485
|
+
&resolveError);
|
|
486
|
+
ThrowIfFailed(ok, resolveError);
|
|
487
|
+
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
488
|
+
if (capturedError != nil) {
|
|
489
|
+
throw NSErrorException(capturedError);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
NativePrivateRecordingHandle::NativePrivateRecordingHandle(const Napi::CallbackInfo& info)
|
|
496
|
+
: Napi::ObjectWrap<NativePrivateRecordingHandle>(info) {
|
|
497
|
+
device_ = UnwrapExternalId(info);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
void NativePrivateRecordingHandle::Init(Napi::Env env) {
|
|
501
|
+
Napi::Function ctor = DefineClass(env, "NativePrivateRecordingHandle",
|
|
502
|
+
{
|
|
503
|
+
InstanceMethod<&NativePrivateRecordingHandle::Stop>("stop"),
|
|
504
|
+
});
|
|
505
|
+
env.GetInstanceData<AddonInstanceData>()->privateRecordingConstructor = Napi::Persistent(ctor);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
Napi::Object NativePrivateRecordingHandle::NewInstance(Napi::Env env, id device) {
|
|
509
|
+
return WrapExternalId(env, env.GetInstanceData<AddonInstanceData>()->privateRecordingConstructor, device);
|
|
510
|
+
}
|
|
511
|
+
|
|
295
512
|
class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
|
|
296
513
|
public:
|
|
297
514
|
static void Init(Napi::Env env);
|
|
@@ -826,15 +1043,69 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
|
|
|
826
1043
|
[](Napi::Env env, NSArray* result) -> Napi::Value { return NSObjectToJsValue(env, result); });
|
|
827
1044
|
}
|
|
828
1045
|
|
|
829
|
-
//
|
|
830
|
-
//
|
|
1046
|
+
// Shared by StartVideoRecording (audio path)/StartVideoStream — `argIndex` is where the options
|
|
1047
|
+
// object (if any) sits in `info`.
|
|
1048
|
+
static coresim::VideoEncoderOptions ParseVideoEncoderOptions(const Napi::CallbackInfo& info, size_t argIndex) {
|
|
1049
|
+
NSString* displayId = nil;
|
|
1050
|
+
coresim::VideoStreamCodec codec = coresim::VideoStreamCodec::kH264;
|
|
1051
|
+
double fps = 15.0;
|
|
1052
|
+
int bitrate = 2000000;
|
|
1053
|
+
if (info.Length() > argIndex && info[argIndex].IsObject()) {
|
|
1054
|
+
Napi::Object options = info[argIndex].As<Napi::Object>();
|
|
1055
|
+
if (options.Has("displayId") && options.Get("displayId").IsString()) {
|
|
1056
|
+
displayId = @(options.Get("displayId").As<Napi::String>().Utf8Value().c_str());
|
|
1057
|
+
}
|
|
1058
|
+
if (options.Has("codec") && options.Get("codec").IsString() &&
|
|
1059
|
+
options.Get("codec").As<Napi::String>().Utf8Value() == "hevc") {
|
|
1060
|
+
codec = coresim::VideoStreamCodec::kHEVC;
|
|
1061
|
+
}
|
|
1062
|
+
if (options.Has("fps") && options.Get("fps").IsNumber()) {
|
|
1063
|
+
fps = options.Get("fps").As<Napi::Number>().DoubleValue();
|
|
1064
|
+
}
|
|
1065
|
+
if (options.Has("bitrate") && options.Get("bitrate").IsNumber()) {
|
|
1066
|
+
bitrate = options.Get("bitrate").As<Napi::Number>().Int32Value();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return coresim::VideoEncoderOptions{codec, displayId, fps, bitrate};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
static bool OptionsWantAudio(const Napi::CallbackInfo& info, size_t argIndex) {
|
|
1073
|
+
return info.Length() > argIndex && info[argIndex].IsObject() && info[argIndex].As<Napi::Object>().Has("audio") &&
|
|
1074
|
+
info[argIndex].As<Napi::Object>().Get("audio").IsBoolean() &&
|
|
1075
|
+
info[argIndex].As<Napi::Object>().Get("audio").As<Napi::Boolean>().Value();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
static bool OptionsHasFps(const Napi::CallbackInfo& info, size_t argIndex) {
|
|
1079
|
+
return info.Length() > argIndex && info[argIndex].IsObject() && info[argIndex].As<Napi::Object>().Has("fps") &&
|
|
1080
|
+
info[argIndex].As<Napi::Object>().Get("fps").IsNumber();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Mirrors `simctl io <udid> recordVideo` by default — CoreSimulator's own private, video-only
|
|
1084
|
+
// recorder (see sim_video_recording.mm), which supports `mask`/`bitrate` but has no polling loop
|
|
1085
|
+
// for `fps` to cap. With `options.audio` OR an explicit `options.fps` (which only means something
|
|
1086
|
+
// against a real polling loop), drives this addon's own encoders instead (av_recording.h) — video
|
|
1087
|
+
// only when `fps` was the sole reason, video+audio when `audio` was set too — since the private
|
|
1088
|
+
// recorder has no per-frame hook to mux audio into and no `fps` knob either way. `mask` is not
|
|
1089
|
+
// supported on this path. Either way returns a handle (NativePrivateRecordingHandle or
|
|
1090
|
+
// NativeAVRecording — see their own doc comments) exposing the identical `stop()` shape, so the
|
|
1091
|
+
// caller never needs to know which path it took.
|
|
831
1092
|
Napi::Value StartVideoRecording(const Napi::CallbackInfo& info) {
|
|
1093
|
+
NSString* outputFile = @(info[0].As<Napi::String>().Utf8Value().c_str());
|
|
1094
|
+
bool wantsAudio = OptionsWantAudio(info, 1);
|
|
1095
|
+
if (wantsAudio || OptionsHasFps(info, 1)) {
|
|
1096
|
+
return StartAVRecording(info, outputFile, wantsAudio);
|
|
1097
|
+
}
|
|
1098
|
+
return StartPrivateVideoRecording(info, outputFile);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Resolves once the first frame is recorded — see CLAUDE.md for the race hit by calling this
|
|
1102
|
+
// handle's own stop() any earlier.
|
|
1103
|
+
Napi::Value StartPrivateVideoRecording(const Napi::CallbackInfo& info, NSString* outputFile) {
|
|
832
1104
|
Napi::Env env = info.Env();
|
|
833
1105
|
id device = device_;
|
|
834
|
-
NSString* outputFile = @(info[0].As<Napi::String>().Utf8Value().c_str());
|
|
835
1106
|
NSString* displayId = nil;
|
|
836
1107
|
coresim::VideoMaskPolicy mask = coresim::VideoMaskPolicy::kIgnored;
|
|
837
|
-
|
|
1108
|
+
NSMutableDictionary* assetWriterOutputSettings = [NSMutableDictionary dictionary];
|
|
838
1109
|
if (info.Length() > 1 && info[1].IsObject()) {
|
|
839
1110
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
840
1111
|
if (options.Has("displayId") && options.Get("displayId").IsString()) {
|
|
@@ -850,100 +1121,204 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
|
|
|
850
1121
|
}
|
|
851
1122
|
if (options.Has("codec") && options.Get("codec").IsString()) {
|
|
852
1123
|
std::string codecValue = options.Get("codec").As<Napi::String>().Utf8Value();
|
|
853
|
-
|
|
854
|
-
assetWriterOutputSettings = @{AVVideoCodecKey : codecType};
|
|
1124
|
+
assetWriterOutputSettings[AVVideoCodecKey] = codecValue == "hevc" ? AVVideoCodecTypeHEVC : AVVideoCodecTypeH264;
|
|
855
1125
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.recordVideo", DISPATCH_QUEUE_SERIAL);
|
|
860
|
-
__block NSError* capturedError = nil;
|
|
861
|
-
NSError* resolveError = nil;
|
|
862
|
-
BOOL ok = coresim::StartVideoRecording(
|
|
863
|
-
device, displayId, mask, assetWriterOutputSettings, outputFile, queue,
|
|
864
|
-
^(NSError* asyncError) {
|
|
865
|
-
capturedError = asyncError;
|
|
866
|
-
dispatch_semaphore_signal(sema);
|
|
867
|
-
},
|
|
868
|
-
&resolveError);
|
|
869
|
-
ThrowIfFailed(ok, resolveError);
|
|
870
|
-
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
871
|
-
if (capturedError != nil) {
|
|
872
|
-
throw NSErrorException(capturedError);
|
|
1126
|
+
if (options.Has("bitrate") && options.Get("bitrate").IsNumber()) {
|
|
1127
|
+
int bitrate = options.Get("bitrate").As<Napi::Number>().Int32Value();
|
|
1128
|
+
assetWriterOutputSettings[AVVideoCompressionPropertiesKey] = @{AVVideoAverageBitRateKey : @(bitrate)};
|
|
873
1129
|
}
|
|
874
|
-
}
|
|
1130
|
+
}
|
|
1131
|
+
return RunAsync<id>(
|
|
1132
|
+
env,
|
|
1133
|
+
[device, displayId, mask, assetWriterOutputSettings, outputFile]() -> id {
|
|
1134
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
1135
|
+
dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.recordVideo", DISPATCH_QUEUE_SERIAL);
|
|
1136
|
+
__block NSError* capturedError = nil;
|
|
1137
|
+
NSError* resolveError = nil;
|
|
1138
|
+
BOOL ok = coresim::StartVideoRecording(
|
|
1139
|
+
device, displayId, mask, assetWriterOutputSettings, outputFile, queue,
|
|
1140
|
+
^(NSError* asyncError) {
|
|
1141
|
+
capturedError = asyncError;
|
|
1142
|
+
dispatch_semaphore_signal(sema);
|
|
1143
|
+
},
|
|
1144
|
+
&resolveError);
|
|
1145
|
+
ThrowIfFailed(ok, resolveError);
|
|
1146
|
+
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
1147
|
+
if (capturedError != nil) {
|
|
1148
|
+
throw NSErrorException(capturedError);
|
|
1149
|
+
}
|
|
1150
|
+
return device;
|
|
1151
|
+
},
|
|
1152
|
+
[](Napi::Env env, id device) -> Napi::Value { return NativePrivateRecordingHandle::NewInstance(env, device); });
|
|
875
1153
|
}
|
|
876
1154
|
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
|
|
1155
|
+
// Recording via this addon's own encoders — see av_recording.h. Combined audio+video when
|
|
1156
|
+
// `captureAudio` is set, video-only (via the same VideoFrameEncoder, just no audio tap/track)
|
|
1157
|
+
// when it's not — reached with `captureAudio == false` when `fps` was requested without `audio`
|
|
1158
|
+
// (see StartVideoRecording). Resolves once the first sample (video, or whichever of video/audio
|
|
1159
|
+
// comes first when both are captured) is written.
|
|
1160
|
+
Napi::Value StartAVRecording(const Napi::CallbackInfo& info, NSString* outputFile, bool captureAudio) {
|
|
880
1161
|
Napi::Env env = info.Env();
|
|
881
1162
|
id device = device_;
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1163
|
+
NSString* udid = DeviceUDID(device).UUIDString;
|
|
1164
|
+
coresim::VideoEncoderOptions videoOptions = ParseVideoEncoderOptions(info, 1);
|
|
1165
|
+
Napi::Function onError = info[2].As<Napi::Function>();
|
|
1166
|
+
|
|
1167
|
+
// Released either from within onEnd_ (a live failure, which fires at most once per session —
|
|
1168
|
+
// see av_recording.h) or, if the recording fails before ever starting, right below instead
|
|
1169
|
+
// (onEnd_ never fires for a Start() that never got past its own setup).
|
|
1170
|
+
Napi::ThreadSafeFunction errorTsfn =
|
|
1171
|
+
Napi::ThreadSafeFunction::New(env, onError, "coresim AV recording error", 0, 1);
|
|
1172
|
+
|
|
1173
|
+
return RunAsync<std::shared_ptr<coresim::AVRecordingSession>>(
|
|
1174
|
+
env,
|
|
1175
|
+
[device, udid, videoOptions, outputFile, captureAudio,
|
|
1176
|
+
errorTsfn]() mutable -> std::shared_ptr<coresim::AVRecordingSession> {
|
|
1177
|
+
auto session =
|
|
1178
|
+
std::make_shared<coresim::AVRecordingSession>(device, udid, videoOptions, outputFile, captureAudio);
|
|
1179
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
1180
|
+
auto resolved = std::make_shared<std::atomic<bool>>(false);
|
|
1181
|
+
// Only ever written before `sema` is signaled on the "failed before starting" path
|
|
1182
|
+
// below, so `work`'s frame (and this variable) is always still alive when it happens.
|
|
1183
|
+
NSError* startupError = nil;
|
|
1184
|
+
try {
|
|
1185
|
+
session->Start(
|
|
1186
|
+
[sema, resolved]() mutable {
|
|
1187
|
+
resolved->store(true);
|
|
1188
|
+
dispatch_semaphore_signal(sema);
|
|
1189
|
+
},
|
|
1190
|
+
[errorTsfn, resolved, sema, &startupError](NSError* error) mutable {
|
|
1191
|
+
if (!resolved->exchange(true)) {
|
|
1192
|
+
// Failed before ever starting — reported via this call's own rejection below,
|
|
1193
|
+
// not the live error channel (nothing has "started" yet to report a live
|
|
1194
|
+
// failure for).
|
|
1195
|
+
startupError = error;
|
|
1196
|
+
dispatch_semaphore_signal(sema);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
NSErrorException exception(error);
|
|
1200
|
+
errorTsfn.BlockingCall([exception](Napi::Env env, Napi::Function jsCallback) {
|
|
1201
|
+
// See StartVideoStream's identical comment on why this is wrapped in try/catch.
|
|
1202
|
+
try {
|
|
1203
|
+
jsCallback.Call({NSErrorExceptionToJsError(env, exception).Value()});
|
|
1204
|
+
} catch (...) {
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
},
|
|
1208
|
+
[errorTsfn]() mutable { errorTsfn.Release(); });
|
|
1209
|
+
} catch (...) {
|
|
1210
|
+
errorTsfn.Release();
|
|
1211
|
+
throw;
|
|
1212
|
+
}
|
|
1213
|
+
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
1214
|
+
if (startupError != nil) {
|
|
1215
|
+
throw NSErrorException(startupError);
|
|
1216
|
+
}
|
|
1217
|
+
return session;
|
|
1218
|
+
},
|
|
1219
|
+
[](Napi::Env env, std::shared_ptr<coresim::AVRecordingSession> session) -> Napi::Value {
|
|
1220
|
+
return NativeAVRecording::NewInstance(env, session);
|
|
1221
|
+
});
|
|
900
1222
|
}
|
|
901
1223
|
|
|
902
1224
|
// Real-time encoding via public VideoToolbox APIs (see sim_video_stream.mm) — no private API,
|
|
903
|
-
// no file. `
|
|
904
|
-
//
|
|
1225
|
+
// no file. With `options.audio`, also drives this addon's Core Audio + AudioToolbox encoder
|
|
1226
|
+
// (av_stream.h), interleaving audio units into the same delivered sequence (each tagged
|
|
1227
|
+
// `track`). `onAccessUnit`/`onError` are invoked live for as long as the stream runs; the
|
|
1228
|
+
// returned handle (NativeVideoStream or NativeAVStream — identical `stop()`/`requestKeyFrame()`
|
|
1229
|
+
// shape either way) is all the caller needs, regardless of which path it took.
|
|
905
1230
|
Napi::Value StartVideoStream(const Napi::CallbackInfo& info) {
|
|
906
1231
|
Napi::Env env = info.Env();
|
|
907
1232
|
id device = device_;
|
|
908
|
-
|
|
909
|
-
coresim::VideoStreamCodec codec = coresim::VideoStreamCodec::kH264;
|
|
910
|
-
double fps = 15.0;
|
|
911
|
-
int bitrate = 2000000;
|
|
912
|
-
if (info.Length() > 0 && info[0].IsObject()) {
|
|
913
|
-
Napi::Object options = info[0].As<Napi::Object>();
|
|
914
|
-
if (options.Has("displayId") && options.Get("displayId").IsString()) {
|
|
915
|
-
displayId = @(options.Get("displayId").As<Napi::String>().Utf8Value().c_str());
|
|
916
|
-
}
|
|
917
|
-
if (options.Has("codec") && options.Get("codec").IsString() &&
|
|
918
|
-
options.Get("codec").As<Napi::String>().Utf8Value() == "hevc") {
|
|
919
|
-
codec = coresim::VideoStreamCodec::kHEVC;
|
|
920
|
-
}
|
|
921
|
-
if (options.Has("fps") && options.Get("fps").IsNumber()) {
|
|
922
|
-
fps = options.Get("fps").As<Napi::Number>().DoubleValue();
|
|
923
|
-
}
|
|
924
|
-
if (options.Has("bitrate") && options.Get("bitrate").IsNumber()) {
|
|
925
|
-
bitrate = options.Get("bitrate").As<Napi::Number>().Int32Value();
|
|
926
|
-
}
|
|
927
|
-
}
|
|
1233
|
+
coresim::VideoEncoderOptions options = ParseVideoEncoderOptions(info, 0);
|
|
928
1234
|
Napi::Function onAccessUnit = info[1].As<Napi::Function>();
|
|
929
1235
|
Napi::Function onError = info[2].As<Napi::Function>();
|
|
930
1236
|
|
|
931
1237
|
// Must be constructed on the main thread, like Spawn's own exitTsfn above; released exactly
|
|
932
|
-
// once each, via onEnd (see sim_video_stream.h).
|
|
1238
|
+
// once each, via onEnd (see sim_video_stream.h/av_stream.h).
|
|
933
1239
|
//
|
|
934
1240
|
// accessUnitTsfn's queue is bounded, unlike every other one-shot-callback ThreadSafeFunction
|
|
935
1241
|
// in this addon — a slow-draining consumer would otherwise let queued frame buffers grow
|
|
936
|
-
// unbounded; BlockingCall below naturally throttles the encoder once this fills instead.
|
|
1242
|
+
// unbounded; BlockingCall below naturally throttles the encoder(s) once this fills instead.
|
|
937
1243
|
static constexpr size_t kAccessUnitQueueSize = 60;
|
|
938
1244
|
Napi::ThreadSafeFunction accessUnitTsfn =
|
|
939
1245
|
Napi::ThreadSafeFunction::New(env, onAccessUnit, "coresim video stream access unit", kAccessUnitQueueSize, 1);
|
|
940
1246
|
Napi::ThreadSafeFunction errorTsfn =
|
|
941
1247
|
Napi::ThreadSafeFunction::New(env, onError, "coresim video stream error", 0, 1);
|
|
1248
|
+
// See TsfnReleaseGuard's own comment — shared between onEnd's release and AbortDelivery's
|
|
1249
|
+
// abort below so exactly one of them ever actually runs.
|
|
1250
|
+
auto tsfnGuard = std::make_shared<TsfnReleaseGuard>();
|
|
1251
|
+
|
|
1252
|
+
if (OptionsWantAudio(info, 0)) {
|
|
1253
|
+
NSString* udid = DeviceUDID(device).UUIDString;
|
|
1254
|
+
return RunAsync<std::shared_ptr<coresim::AVStreamSession>>(
|
|
1255
|
+
env,
|
|
1256
|
+
[device, udid, options, accessUnitTsfn, errorTsfn,
|
|
1257
|
+
tsfnGuard]() mutable -> std::shared_ptr<coresim::AVStreamSession> {
|
|
1258
|
+
auto session = std::make_shared<coresim::AVStreamSession>(
|
|
1259
|
+
device, udid, options,
|
|
1260
|
+
[accessUnitTsfn](coresim::AVAccessUnit unit) mutable {
|
|
1261
|
+
accessUnitTsfn.BlockingCall(
|
|
1262
|
+
[unit = std::move(unit)](Napi::Env env, Napi::Function jsCallback) mutable {
|
|
1263
|
+
// See the audio-less branch's identical comment on why this is wrapped in
|
|
1264
|
+
// try/catch.
|
|
1265
|
+
try {
|
|
1266
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
1267
|
+
obj.Set("track",
|
|
1268
|
+
Napi::String::New(env, unit.track == coresim::AVTrack::kVideo ? "video" : "audio"));
|
|
1269
|
+
obj.Set("data", Napi::Buffer<uint8_t>::Copy(env, unit.data.data(), unit.data.size()));
|
|
1270
|
+
obj.Set("isKeyFrame", Napi::Boolean::New(env, unit.isKeyFrame));
|
|
1271
|
+
obj.Set("sequence", Napi::Number::New(env, static_cast<double>(unit.sequence)));
|
|
1272
|
+
obj.Set("timestampMicros", Napi::Number::New(env, static_cast<double>(unit.timestampMicros)));
|
|
1273
|
+
jsCallback.Call({obj});
|
|
1274
|
+
} catch (...) {
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
},
|
|
1278
|
+
[errorTsfn](NSError* error) mutable {
|
|
1279
|
+
NSErrorException exception(error);
|
|
1280
|
+
errorTsfn.BlockingCall([exception](Napi::Env env, Napi::Function jsCallback) {
|
|
1281
|
+
try {
|
|
1282
|
+
jsCallback.Call({NSErrorExceptionToJsError(env, exception).Value()});
|
|
1283
|
+
} catch (...) {
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
},
|
|
1287
|
+
[accessUnitTsfn, errorTsfn, tsfnGuard]() mutable {
|
|
1288
|
+
ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
|
|
1289
|
+
accessUnitTsfn.Release();
|
|
1290
|
+
errorTsfn.Release();
|
|
1291
|
+
});
|
|
1292
|
+
},
|
|
1293
|
+
[accessUnitTsfn, errorTsfn, tsfnGuard]() mutable {
|
|
1294
|
+
// See ActiveSessionRegistry::StopAll's use of AbortDelivery — unblocks a
|
|
1295
|
+
// producer thread stuck pushing into a full queue so Stop() doesn't deadlock
|
|
1296
|
+
// waiting on it.
|
|
1297
|
+
ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
|
|
1298
|
+
accessUnitTsfn.Abort();
|
|
1299
|
+
errorTsfn.Abort();
|
|
1300
|
+
});
|
|
1301
|
+
});
|
|
1302
|
+
try {
|
|
1303
|
+
session->Start();
|
|
1304
|
+
} catch (...) {
|
|
1305
|
+
ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
|
|
1306
|
+
accessUnitTsfn.Release();
|
|
1307
|
+
errorTsfn.Release();
|
|
1308
|
+
});
|
|
1309
|
+
throw;
|
|
1310
|
+
}
|
|
1311
|
+
return session;
|
|
1312
|
+
},
|
|
1313
|
+
[](Napi::Env env, std::shared_ptr<coresim::AVStreamSession> session) -> Napi::Value {
|
|
1314
|
+
return NativeAVStream::NewInstance(env, session);
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
942
1317
|
|
|
943
|
-
coresim::VideoStreamOptions options{codec, displayId, fps, bitrate};
|
|
944
1318
|
return RunAsync<std::shared_ptr<coresim::VideoStreamSession>>(
|
|
945
1319
|
env,
|
|
946
|
-
[device, options, accessUnitTsfn, errorTsfn
|
|
1320
|
+
[device, options, accessUnitTsfn, errorTsfn,
|
|
1321
|
+
tsfnGuard]() mutable -> std::shared_ptr<coresim::VideoStreamSession> {
|
|
947
1322
|
auto session = std::make_shared<coresim::VideoStreamSession>(
|
|
948
1323
|
device, options,
|
|
949
1324
|
[accessUnitTsfn](coresim::VideoAccessUnit unit) mutable {
|
|
@@ -956,6 +1331,7 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
|
|
|
956
1331
|
// anymore. See CLAUDE.md.
|
|
957
1332
|
try {
|
|
958
1333
|
Napi::Object obj = Napi::Object::New(env);
|
|
1334
|
+
obj.Set("track", Napi::String::New(env, "video"));
|
|
959
1335
|
obj.Set("data", Napi::Buffer<uint8_t>::Copy(env, unit.data.data(), unit.data.size()));
|
|
960
1336
|
obj.Set("isKeyFrame", Napi::Boolean::New(env, unit.isKeyFrame));
|
|
961
1337
|
obj.Set("sequence", Napi::Number::New(env, static_cast<double>(unit.sequence)));
|
|
@@ -975,15 +1351,26 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
|
|
|
975
1351
|
}
|
|
976
1352
|
});
|
|
977
1353
|
},
|
|
978
|
-
[accessUnitTsfn, errorTsfn]() mutable {
|
|
979
|
-
|
|
980
|
-
|
|
1354
|
+
[accessUnitTsfn, errorTsfn, tsfnGuard]() mutable {
|
|
1355
|
+
ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
|
|
1356
|
+
accessUnitTsfn.Release();
|
|
1357
|
+
errorTsfn.Release();
|
|
1358
|
+
});
|
|
1359
|
+
},
|
|
1360
|
+
[accessUnitTsfn, errorTsfn, tsfnGuard]() mutable {
|
|
1361
|
+
// See the audio branch's identical comment above.
|
|
1362
|
+
ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
|
|
1363
|
+
accessUnitTsfn.Abort();
|
|
1364
|
+
errorTsfn.Abort();
|
|
1365
|
+
});
|
|
981
1366
|
});
|
|
982
1367
|
try {
|
|
983
1368
|
session->Start();
|
|
984
1369
|
} catch (...) {
|
|
985
|
-
|
|
986
|
-
|
|
1370
|
+
ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
|
|
1371
|
+
accessUnitTsfn.Release();
|
|
1372
|
+
errorTsfn.Release();
|
|
1373
|
+
});
|
|
987
1374
|
throw;
|
|
988
1375
|
}
|
|
989
1376
|
return session;
|
|
@@ -1179,7 +1566,6 @@ void NativeDevice::Init(Napi::Env env) {
|
|
|
1179
1566
|
InstanceMethod<&NativeDevice::Screenshot>("screenshot"),
|
|
1180
1567
|
InstanceMethod<&NativeDevice::GetDisplays>("getDisplays"),
|
|
1181
1568
|
InstanceMethod<&NativeDevice::StartVideoRecording>("startVideoRecording"),
|
|
1182
|
-
InstanceMethod<&NativeDevice::StopVideoRecording>("stopVideoRecording"),
|
|
1183
1569
|
InstanceMethod<&NativeDevice::StartVideoStream>("startVideoStream"),
|
|
1184
1570
|
InstanceMethod<&NativeDevice::Spawn>("spawn"),
|
|
1185
1571
|
});
|
|
@@ -1406,24 +1792,67 @@ Napi::Value FrameworkVersionBinding(const Napi::CallbackInfo& info) {
|
|
|
1406
1792
|
[](Napi::Env env, std::string version) -> Napi::Value { return Napi::String::New(env, version); });
|
|
1407
1793
|
}
|
|
1408
1794
|
|
|
1795
|
+
// Node force-releases any ThreadSafeFunctions still outstanding when an Environment (e.g. a
|
|
1796
|
+
// worker_threads Worker) tears down — racing our own release of the same TSFNs (fired
|
|
1797
|
+
// asynchronously from NativeVideoStream/NativeAVStream/NativeAVRecording::Finalize, or never, if
|
|
1798
|
+
// a running session's JS wrapper was never explicitly stopped) crashes the process. Cleanup hooks
|
|
1799
|
+
// are guaranteed to run before that automatic TSFN teardown, so stopping every active session
|
|
1800
|
+
// here — synchronously, blocking until each has released its own TSFNs — establishes the
|
|
1801
|
+
// ordering Node itself doesn't.
|
|
1802
|
+
//
|
|
1803
|
+
// Also called directly (not just as a cleanup hook) by FlushActiveSessionsBinding below — cleanup
|
|
1804
|
+
// hooks are confirmed (empirically, not just per docs) to NOT run at all for a `process.exit()`
|
|
1805
|
+
// on the main process/thread (unlike a Worker's `worker.terminate()`, or the main thread's own
|
|
1806
|
+
// natural empty-event-loop exit, both of which do invoke them). Without a second, JS-`'exit'`-
|
|
1807
|
+
// event-triggered path to this same function, a forgotten (never explicitly stopped) AV recording
|
|
1808
|
+
// silently loses its AVAssetWriter-buffered data — the file is left with no moov atom — the
|
|
1809
|
+
// instant a caller force-exits via `process.exit()`, since nothing ever calls finishWriting.
|
|
1810
|
+
void CleanupActiveSessions(AddonInstanceData* instanceData) {
|
|
1811
|
+
// AbortDelivery() first: this runs on the main JS thread with the event loop not being pumped
|
|
1812
|
+
// (a cleanup hook, or the synchronous process.on('exit') path below), so nothing else can drain
|
|
1813
|
+
// accessUnitTsfn's bounded queue — a producer thread already blocked pushing into a full queue
|
|
1814
|
+
// would otherwise deadlock against Stop()'s own wait on that same producer thread.
|
|
1815
|
+
auto stopStream = [](auto& session) {
|
|
1816
|
+
session->AbortDelivery();
|
|
1817
|
+
session->Stop();
|
|
1818
|
+
};
|
|
1819
|
+
instanceData->activeVideoStreams.StopAll(stopStream);
|
|
1820
|
+
instanceData->activeAVStreams.StopAll(stopStream);
|
|
1821
|
+
instanceData->activeAVRecordings.StopAll([](auto& session) {
|
|
1822
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
1823
|
+
session->Stop([sema](NSError*) { dispatch_semaphore_signal(sema); });
|
|
1824
|
+
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Deliberately synchronous (not RunAsync/Promise-returning) — meant to be called from a JS-level
|
|
1829
|
+
// `process.on('exit', ...)` listener (native-simctl.ts), which per Node's own contract may only
|
|
1830
|
+
// run synchronous code, but that includes a plain blocking native call like this one (it isn't
|
|
1831
|
+
// scheduling new async JS work, just blocking the calling thread on GCD semaphores that are
|
|
1832
|
+
// serviced by GCD's own independent thread pool — unaffected by anything happening to Node's own
|
|
1833
|
+
// event loop or threadpool during exit). See CleanupActiveSessions's own comment for why this
|
|
1834
|
+
// second call path exists at all.
|
|
1835
|
+
Napi::Value FlushActiveSessionsBinding(const Napi::CallbackInfo& info) {
|
|
1836
|
+
CleanupActiveSessions(info.Env().GetInstanceData<AddonInstanceData>());
|
|
1837
|
+
return info.Env().Undefined();
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1409
1840
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1410
1841
|
// Runs once per Environment (see AddonInstanceData above) — never shared across a
|
|
1411
1842
|
// worker_threads instance also `require()`-ing this addon.
|
|
1412
1843
|
auto* instanceData = new AddonInstanceData();
|
|
1413
1844
|
env.SetInstanceData(instanceData);
|
|
1414
|
-
|
|
1415
|
-
// worker_threads Worker) tears down — racing our own release of the same TSFNs (fired
|
|
1416
|
-
// asynchronously from NativeVideoStream::Finalize, or never, if a running stream's JS wrapper
|
|
1417
|
-
// was never explicitly stopped) crashes the process. Cleanup hooks are guaranteed to run before
|
|
1418
|
-
// that automatic TSFN teardown, so stopping every active stream here — synchronously, blocking
|
|
1419
|
-
// until each has released its own TSFNs — establishes the ordering Node itself doesn't.
|
|
1420
|
-
env.AddCleanupHook(StopAllActiveStreams, instanceData);
|
|
1845
|
+
env.AddCleanupHook(CleanupActiveSessions, instanceData);
|
|
1421
1846
|
NativeDevice::Init(env);
|
|
1422
1847
|
NativeDeviceSet::Init(env);
|
|
1423
1848
|
NativeServiceContext::Init(env);
|
|
1424
1849
|
NativeVideoStream::Init(env);
|
|
1850
|
+
NativeAVStream::Init(env);
|
|
1851
|
+
NativeAVRecording::Init(env);
|
|
1852
|
+
NativePrivateRecordingHandle::Init(env);
|
|
1425
1853
|
exports.Set("sharedServiceContext", Napi::Function::New(env, SharedServiceContextBinding));
|
|
1426
1854
|
exports.Set("frameworkVersion", Napi::Function::New(env, FrameworkVersionBinding));
|
|
1855
|
+
exports.Set("flushActiveSessions", Napi::Function::New(env, FlushActiveSessionsBinding));
|
|
1427
1856
|
return exports;
|
|
1428
1857
|
}
|
|
1429
1858
|
|