@appium/coresim 1.4.0 → 1.6.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -0
  3. package/binding.gyp +14 -1
  4. package/lib/src/commands/video-recording.d.ts +57 -0
  5. package/lib/src/commands/video-recording.d.ts.map +1 -0
  6. package/lib/src/commands/video-recording.js +123 -0
  7. package/lib/src/commands/video-recording.js.map +1 -0
  8. package/lib/src/commands/video-stream.d.ts +92 -0
  9. package/lib/src/commands/video-stream.d.ts.map +1 -0
  10. package/lib/src/commands/video-stream.js +242 -0
  11. package/lib/src/commands/video-stream.js.map +1 -0
  12. package/lib/src/index.d.ts +2 -1
  13. package/lib/src/index.d.ts.map +1 -1
  14. package/lib/src/index.js +1 -0
  15. package/lib/src/index.js.map +1 -1
  16. package/lib/src/native-simctl.d.ts +4 -0
  17. package/lib/src/native-simctl.d.ts.map +1 -1
  18. package/lib/src/native-simctl.js +21 -1
  19. package/lib/src/native-simctl.js.map +1 -1
  20. package/lib/src/types.d.ts +134 -0
  21. package/lib/src/types.d.ts.map +1 -1
  22. package/lib/src/utils/index.d.ts +1 -1
  23. package/lib/src/utils/index.d.ts.map +1 -1
  24. package/lib/src/utils/index.js +1 -1
  25. package/lib/src/utils/index.js.map +1 -1
  26. package/lib/src/utils/run-catching.d.ts +4 -0
  27. package/lib/src/utils/run-catching.d.ts.map +1 -1
  28. package/lib/src/utils/run-catching.js +11 -0
  29. package/lib/src/utils/run-catching.js.map +1 -1
  30. package/package.json +1 -1
  31. package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
  32. package/src/commands/video-recording.ts +158 -0
  33. package/src/commands/video-stream.ts +283 -0
  34. package/src/coresim.mm +708 -1
  35. package/src/index.ts +4 -0
  36. package/src/native/audio_encoder.h +65 -0
  37. package/src/native/audio_encoder.mm +270 -0
  38. package/src/native/av_recording.h +52 -0
  39. package/src/native/av_recording.mm +424 -0
  40. package/src/native/av_stream.h +70 -0
  41. package/src/native/av_stream.mm +217 -0
  42. package/src/native/monotonic_clock.h +20 -0
  43. package/src/native/sim_audio_tap.h +67 -0
  44. package/src/native/sim_audio_tap.mm +350 -0
  45. package/src/native/sim_process.h +10 -0
  46. package/src/native/sim_process.mm +81 -18
  47. package/src/native/sim_screenshot.h +10 -0
  48. package/src/native/sim_screenshot.mm +11 -6
  49. package/src/native/sim_video_recording.h +27 -0
  50. package/src/native/sim_video_recording.mm +87 -0
  51. package/src/native/sim_video_stream.h +64 -0
  52. package/src/native/sim_video_stream.mm +71 -0
  53. package/src/native/video_encoder.h +76 -0
  54. package/src/native/video_encoder.mm +417 -0
  55. package/src/native-simctl.ts +22 -1
  56. package/src/types.ts +149 -0
  57. package/src/utils/index.ts +1 -1
  58. package/src/utils/run-catching.ts +11 -0
@@ -0,0 +1,424 @@
1
+ #include "av_recording.h"
2
+
3
+ #include <atomic>
4
+ #include <mutex>
5
+ #include <vector>
6
+
7
+ #include "audio_encoder.h"
8
+ #include "monotonic_clock.h"
9
+ #include "nserror_bridge.h"
10
+ #include "sim_audio_tap.h"
11
+
12
+ namespace coresim {
13
+
14
+ namespace {
15
+
16
+ NSString* const kAVRecordingErrorDomain = @"com.appium.coresim.AVRecording";
17
+
18
+ NSError* MakeError(NSInteger code, NSString* message) {
19
+ return [NSError errorWithDomain:kAVRecordingErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
20
+ }
21
+
22
+ constexpr size_t kMaxPendingAudioSamples = 500; // a few seconds of AAC packets — see HandleAudioSample
23
+
24
+ } // namespace
25
+
26
+ class AVRecordingSession::Impl {
27
+ public:
28
+ Impl(id device, NSString* udid, VideoEncoderOptions videoOptions, NSString* outputFile, bool captureAudio)
29
+ : device_(device),
30
+ udid_(udid),
31
+ videoOptions_(videoOptions),
32
+ outputFile_(outputFile),
33
+ captureAudio_(captureAudio) {}
34
+
35
+ ~Impl() { TearDownIfNeeded(); }
36
+
37
+ void Start(std::function<void()> onFirstSample, std::function<void(NSError*)> onError, std::function<void()> onEnd) {
38
+ onFirstSample_ = std::move(onFirstSample);
39
+ onError_ = std::move(onError);
40
+ onEnd_ = std::move(onEnd);
41
+ clockOrigin_ = MonotonicSeconds();
42
+
43
+ [[NSFileManager defaultManager] removeItemAtPath:outputFile_ error:nil];
44
+ NSError* writerError = nil;
45
+ writer_ = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:outputFile_]
46
+ fileType:AVFileTypeMPEG4
47
+ error:&writerError];
48
+ if (writer_ == nil) {
49
+ throw NSErrorException(writerError ?: MakeError(1, @"Failed to create an AVAssetWriter"));
50
+ }
51
+
52
+ try {
53
+ if (captureAudio_) {
54
+ audioTap_ = std::make_unique<AudioTapSession>(
55
+ udid_, [this](const AudioBufferList* data, const AudioTimeStamp* time) { HandleAudioPCM(data, time); },
56
+ [this](NSError* error) {
57
+ // A failed process-list refresh (kAudioTapNonFatalProcessListRefreshErrorCode) isn't
58
+ // fatal — the tap keeps running. Everything else is.
59
+ if ([error.domain isEqualToString:kAudioTapErrorDomain] &&
60
+ error.code == kAudioTapNonFatalProcessListRefreshErrorCode) {
61
+ return;
62
+ }
63
+ Fail(error, /*fromVideo=*/false);
64
+ },
65
+ [] {});
66
+ audioTap_->Start();
67
+
68
+ // Start() can invoke HandleAudioPCM before returning, so audioEncoder_ is written under
69
+ // mutex_ (same lock HandleAudioPCM reads it under) to avoid a torn/racy pointer read.
70
+ auto encoder = std::make_unique<AudioEncoder>(
71
+ audioTap_->Format(), [this](CMSampleBufferRef sampleBuffer) { HandleAudioSample(sampleBuffer); },
72
+ &clockOrigin_);
73
+ {
74
+ std::lock_guard<std::mutex> lock(mutex_);
75
+ audioEncoder_ = std::move(encoder);
76
+ }
77
+ // Audio's format is known immediately (unlike video's, learned from its first sample), so
78
+ // its input can be added to the writer right away — well before startWriting is called.
79
+ audioInput_ = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
80
+ outputSettings:nil
81
+ sourceFormatHint:audioEncoder_->OutputFormatDescription()];
82
+ audioInput_.expectsMediaDataInRealTime = YES;
83
+ [writer_ addInput:audioInput_];
84
+ }
85
+
86
+ videoEncoder_ = std::make_unique<VideoFrameEncoder>(
87
+ device_, videoOptions_, [this](CMSampleBufferRef sampleBuffer) { HandleVideoSample(sampleBuffer); },
88
+ [this](NSError* error) { Fail(error, /*fromVideo=*/true); }, [] {}, &clockOrigin_);
89
+ videoEncoder_->Start();
90
+ } catch (...) {
91
+ TearDownIfNeeded();
92
+ throw;
93
+ }
94
+ }
95
+
96
+ // Must be fully idempotent, including concurrently: the env cleanup hook (coresim.mm) also
97
+ // unconditionally calls Stop() on every still-registered session, including one already stopped
98
+ // moments earlier whose JS wrapper hasn't GC'd yet. Finalizing an already-`.completed`
99
+ // AVAssetWriter throws an uncaught NSException and aborts the process — so only the FIRST call
100
+ // ever touches the writer/encoders; every later (or concurrent) call just replays that outcome.
101
+ void Stop(std::function<void(NSError*)> onFinished) {
102
+ bool isFirstCall;
103
+ bool resultReady;
104
+ NSError* resultError = nil;
105
+ {
106
+ std::lock_guard<std::mutex> lock(mutex_);
107
+ isFirstCall = !stopped_;
108
+ stopped_ = true;
109
+ resultReady = stopResultReady_;
110
+ resultError = stopResultError_;
111
+ if (!isFirstCall && !resultReady) {
112
+ pendingStopCallbacks_.push_back(std::move(onFinished));
113
+ return;
114
+ }
115
+ }
116
+ if (!isFirstCall) {
117
+ if (onFinished) {
118
+ onFinished(resultError);
119
+ }
120
+ return;
121
+ }
122
+
123
+ // Blocks until each has fully torn down — safe to finalize the writer afterward without racing
124
+ // a further sample callback, and also the point past which no new Fail() can start (both
125
+ // encoders' onError channels are now silenced) — which is what makes the stopResultReady_
126
+ // check below race-free against a concurrent Fail() (e.g. the audio tap's own poll timer).
127
+ if (videoEncoder_) {
128
+ videoEncoder_->Stop();
129
+ }
130
+ if (audioTap_) {
131
+ audioTap_->Stop();
132
+ }
133
+
134
+ bool started;
135
+ bool alreadyFailed;
136
+ NSError* failedError = nil;
137
+ {
138
+ std::lock_guard<std::mutex> lock(mutex_);
139
+ started = writerStarted_;
140
+ alreadyFailed = stopResultReady_;
141
+ failedError = stopResultError_;
142
+ ClearPendingAudioLocked();
143
+ }
144
+ if (alreadyFailed) {
145
+ // A concurrent Fail() already recorded the outcome and (if started) cancelled the writer —
146
+ // replay that outcome instead of touching it again; finishWriting on a cancelled writer
147
+ // throws an uncaught NSException.
148
+ if (onFinished) {
149
+ onFinished(failedError);
150
+ }
151
+ FireEndOnce();
152
+ return;
153
+ }
154
+ if (!started) {
155
+ FinishStop(MakeError(2, @"No audio or video was captured before the recording was stopped"),
156
+ std::move(onFinished));
157
+ return;
158
+ }
159
+ [videoInput_ markAsFinished];
160
+ [audioInput_ markAsFinished]; // no-op if `captureAudio_` was never set (audioInput_ stays nil)
161
+ AVAssetWriter* writer = writer_;
162
+ [writer finishWritingWithCompletionHandler:^{
163
+ NSError* finishError = (writer.status == AVAssetWriterStatusCompleted) ? nil : writer.error;
164
+ FinishStop(finishError, std::move(onFinished));
165
+ }];
166
+ }
167
+
168
+ private:
169
+ void HandleVideoSample(CMSampleBufferRef sampleBuffer) {
170
+ NSError* writingError = nil;
171
+ bool justStarted = false;
172
+ {
173
+ std::lock_guard<std::mutex> lock(mutex_);
174
+ if (stopped_) {
175
+ return;
176
+ }
177
+ if (!writerStarted_) {
178
+ CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
179
+ videoInput_ = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
180
+ outputSettings:nil
181
+ sourceFormatHint:format];
182
+ videoInput_.expectsMediaDataInRealTime = YES;
183
+ [writer_ addInput:videoInput_];
184
+ if ([writer_ startWriting]) {
185
+ [writer_ startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
186
+ writerStarted_ = true;
187
+ justStarted = true;
188
+ AppendVideoLocked(sampleBuffer);
189
+ for (CMSampleBufferRef pending : pendingAudio_) {
190
+ AppendAudioLocked(pending);
191
+ CFRelease(pending);
192
+ }
193
+ pendingAudio_.clear();
194
+ } else {
195
+ writingError = writer_.error ?: MakeError(3, @"AVAssetWriter startWriting failed");
196
+ }
197
+ } else {
198
+ AppendVideoLocked(sampleBuffer);
199
+ }
200
+ }
201
+ if (writingError != nil) {
202
+ // Detected while handling a video callback — see Fail()'s doc comment for why this can only
203
+ // safely stop the audio side directly from here, not video's own encoder.
204
+ Fail(writingError, /*fromVideo=*/true);
205
+ return;
206
+ }
207
+ if (justStarted) {
208
+ FireFirstSampleOnce();
209
+ }
210
+ }
211
+
212
+ void HandleAudioSample(CMSampleBufferRef sampleBuffer) {
213
+ std::lock_guard<std::mutex> lock(mutex_);
214
+ if (stopped_) {
215
+ return;
216
+ }
217
+ if (!writerStarted_) {
218
+ // Buffered until video's first sample establishes the writer's format/session (see
219
+ // HandleVideoSample) — capped so a display that never resolves can't grow this unboundedly.
220
+ CFRetain(sampleBuffer);
221
+ pendingAudio_.push_back(sampleBuffer);
222
+ if (pendingAudio_.size() > kMaxPendingAudioSamples) {
223
+ CFRelease(pendingAudio_.front());
224
+ pendingAudio_.erase(pendingAudio_.begin());
225
+ }
226
+ return;
227
+ }
228
+ AppendAudioLocked(sampleBuffer);
229
+ }
230
+
231
+ // Runs on AudioTapSession's own queue; audioEncoder_ is guarded by mutex_ (see Start()). A throw
232
+ // from EncodePCM is left to propagate — AudioTapSession::HandleBuffer catches it and self-stops
233
+ // (see sim_audio_tap.h's onBuffer doc) — this method must never call audioTap_->Stop() itself.
234
+ void HandleAudioPCM(const AudioBufferList* data, const AudioTimeStamp* time) {
235
+ AudioEncoder* encoder;
236
+ {
237
+ std::lock_guard<std::mutex> lock(mutex_);
238
+ if (stopped_) {
239
+ return;
240
+ }
241
+ encoder = audioEncoder_.get();
242
+ }
243
+ if (encoder != nullptr) {
244
+ encoder->EncodePCM(data, time);
245
+ }
246
+ }
247
+
248
+ // Caller holds mutex_.
249
+ void AppendVideoLocked(CMSampleBufferRef sampleBuffer) {
250
+ if (videoInput_.isReadyForMoreMediaData) {
251
+ [videoInput_ appendSampleBuffer:sampleBuffer];
252
+ }
253
+ }
254
+
255
+ // Caller holds mutex_.
256
+ void AppendAudioLocked(CMSampleBufferRef sampleBuffer) {
257
+ if (audioInput_.isReadyForMoreMediaData) {
258
+ [audioInput_ appendSampleBuffer:sampleBuffer];
259
+ }
260
+ }
261
+
262
+ // Caller holds mutex_.
263
+ void ClearPendingAudioLocked() {
264
+ for (CMSampleBufferRef pending : pendingAudio_) {
265
+ CFRelease(pending);
266
+ }
267
+ pendingAudio_.clear();
268
+ }
269
+
270
+ void FireFirstSampleOnce() {
271
+ if (!firstSampleFired_.exchange(true)) {
272
+ if (onFirstSample_) {
273
+ onFirstSample_();
274
+ }
275
+ }
276
+ }
277
+
278
+ void FireEndOnce() {
279
+ if (!endFired_.exchange(true)) {
280
+ if (onEnd_) {
281
+ onEnd_();
282
+ }
283
+ }
284
+ }
285
+
286
+ // Caller holds mutex_. Records the session's single, final outcome — a no-op if already
287
+ // recorded. Returns any Stop() calls queued while the outcome was unknown; caller must invoke
288
+ // each with `error`, unlocked.
289
+ std::vector<std::function<void(NSError*)>> RecordResultLocked(NSError* error) {
290
+ if (stopResultReady_) {
291
+ return {};
292
+ }
293
+ stopResultReady_ = true;
294
+ stopResultError_ = error;
295
+ std::vector<std::function<void(NSError*)>> pending;
296
+ pending.swap(pendingStopCallbacks_);
297
+ return pending;
298
+ }
299
+
300
+ // Caller holds mutex_. Returns any queued Stop() calls to flush (see RecordResultLocked).
301
+ std::vector<std::function<void(NSError*)>> FailLocked(NSError* error) {
302
+ if (stopResultReady_) {
303
+ return {}; // already resolved (a prior Fail() or Stop() completion)
304
+ }
305
+ stopped_ = true;
306
+ if (writerStarted_) {
307
+ [writer_ cancelWriting];
308
+ }
309
+ ClearPendingAudioLocked();
310
+ auto pending = RecordResultLocked(error);
311
+ if (onError_) {
312
+ onError_(error);
313
+ }
314
+ return pending;
315
+ }
316
+
317
+ // Called from either encoder's onError, or from HandleVideoSample on a writer-level failure —
318
+ // not holding mutex_ (could deadlock against that same encoder's own queue). Stops only the
319
+ // OTHER (still-running) encoder — the one whose callback we're inside is already tearing itself
320
+ // down on its own error path, and calling its own blocking Stop() here would deadlock.
321
+ void Fail(NSError* error, bool fromVideo) {
322
+ if (fromVideo) {
323
+ if (audioTap_) {
324
+ audioTap_->Stop();
325
+ }
326
+ } else {
327
+ if (videoEncoder_) {
328
+ videoEncoder_->Stop();
329
+ }
330
+ }
331
+ std::vector<std::function<void(NSError*)>> pending;
332
+ {
333
+ std::lock_guard<std::mutex> lock(mutex_);
334
+ pending = FailLocked(error);
335
+ }
336
+ for (auto& callback : pending) {
337
+ if (callback) {
338
+ callback(error);
339
+ }
340
+ }
341
+ FireEndOnce();
342
+ }
343
+
344
+ // The single point where a real (first-ever) Stop() attempt's outcome becomes known — records
345
+ // it, replies to `onFinished` and any callers that arrived while it was still in flight, then
346
+ // fires onEnd_. Never called a second time for the same session (see Stop()'s own guard).
347
+ void FinishStop(NSError* error, std::function<void(NSError*)> onFinished) {
348
+ std::vector<std::function<void(NSError*)>> pending;
349
+ {
350
+ std::lock_guard<std::mutex> lock(mutex_);
351
+ pending = RecordResultLocked(error);
352
+ }
353
+ if (onFinished) {
354
+ onFinished(error);
355
+ }
356
+ for (auto& callback : pending) {
357
+ if (callback) {
358
+ callback(error);
359
+ }
360
+ }
361
+ FireEndOnce();
362
+ }
363
+
364
+ // Best-effort, synchronous cleanup for the destructor path (Start() throwing partway through,
365
+ // or the session being destroyed without an explicit Stop()) — no completion callback, unlike
366
+ // the public Stop().
367
+ void TearDownIfNeeded() {
368
+ if (videoEncoder_) {
369
+ videoEncoder_->Stop();
370
+ }
371
+ if (audioTap_) {
372
+ audioTap_->Stop();
373
+ }
374
+ std::lock_guard<std::mutex> lock(mutex_);
375
+ if (writerStarted_ && writer_ != nil) {
376
+ [writer_ cancelWriting];
377
+ }
378
+ ClearPendingAudioLocked();
379
+ }
380
+
381
+ id device_;
382
+ NSString* udid_;
383
+ VideoEncoderOptions videoOptions_;
384
+ NSString* outputFile_;
385
+ bool captureAudio_;
386
+
387
+ std::function<void()> onFirstSample_;
388
+ std::function<void(NSError*)> onError_;
389
+ std::function<void()> onEnd_;
390
+
391
+ double clockOrigin_ = 0;
392
+ std::unique_ptr<VideoFrameEncoder> videoEncoder_;
393
+ std::unique_ptr<AudioTapSession> audioTap_;
394
+ std::unique_ptr<AudioEncoder> audioEncoder_;
395
+
396
+ AVAssetWriter* writer_ = nil;
397
+ AVAssetWriterInput* videoInput_ = nil;
398
+ AVAssetWriterInput* audioInput_ = nil;
399
+
400
+ std::mutex mutex_;
401
+ bool writerStarted_ = false;
402
+ bool stopped_ = false; // true from the first Stop() (or Fail()) call onward
403
+ bool stopResultReady_ = false;
404
+ NSError* stopResultError_ = nil;
405
+ std::vector<std::function<void(NSError*)>> pendingStopCallbacks_; // Stop() calls awaiting stopResultReady_
406
+ std::vector<CMSampleBufferRef> pendingAudio_;
407
+ std::atomic<bool> firstSampleFired_{false};
408
+ std::atomic<bool> endFired_{false};
409
+ };
410
+
411
+ AVRecordingSession::AVRecordingSession(id device, NSString* udid, VideoEncoderOptions videoOptions,
412
+ NSString* outputFile, bool captureAudio)
413
+ : impl_(std::make_unique<Impl>(device, udid, videoOptions, outputFile, captureAudio)) {}
414
+
415
+ AVRecordingSession::~AVRecordingSession() = default;
416
+
417
+ void AVRecordingSession::Start(std::function<void()> onFirstSample, std::function<void(NSError*)> onError,
418
+ std::function<void()> onEnd) {
419
+ impl_->Start(std::move(onFirstSample), std::move(onError), std::move(onEnd));
420
+ }
421
+
422
+ void AVRecordingSession::Stop(std::function<void(NSError*)> onFinished) { impl_->Stop(std::move(onFinished)); }
423
+
424
+ } // namespace coresim
@@ -0,0 +1,70 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ #include <cstdint>
6
+ #include <functional>
7
+ #include <memory>
8
+ #include <vector>
9
+
10
+ #include "video_encoder.h"
11
+
12
+ namespace coresim {
13
+
14
+ enum class AVTrack { kVideo, kAudio };
15
+
16
+ // One encoded unit from AVStreamSession::onAccessUnit — either an Annex-B video access unit
17
+ // (same wire format as VideoAccessUnit) or a raw AAC-LC packet, discriminated by `track`.
18
+ // `isKeyFrame` is video-only (always true for audio — every AAC-LC packet is independently
19
+ // decodable); `sequence` counts per track, not globally, since interleaving order between the two
20
+ // tracks carries no meaning of its own — only `timestampMicros`, on the shared session clock
21
+ // (monotonic_clock.h), does.
22
+ struct AVAccessUnit {
23
+ AVTrack track = AVTrack::kVideo;
24
+ std::vector<uint8_t> data;
25
+ bool isKeyFrame = false;
26
+ uint64_t sequence = 0;
27
+ int64_t timestampMicros = 0;
28
+ };
29
+
30
+ // Combined audio+video real-time streaming — the streaming counterpart to AVRecordingSession
31
+ // (av_recording.h). Drives the same VideoFrameEncoder/AudioTapSession/AudioEncoder trio on one
32
+ // shared PTS clock, but delivers each encoded unit live via `onAccessUnit` (interleaved, in
33
+ // production order) instead of muxing into a file.
34
+ //
35
+ // IMPORTANT: needs the host's "System Audio Recording Only" TCC permission — see sim_audio_tap.h.
36
+ class AVStreamSession {
37
+ public:
38
+ // `onAbortDelivery`, if set, is invoked by AbortDelivery() below — not called by this class on
39
+ // its own. See VideoStreamSession's identical parameter for why it exists.
40
+ AVStreamSession(id device, NSString* udid, VideoEncoderOptions videoOptions,
41
+ std::function<void(AVAccessUnit)> onAccessUnit, std::function<void(NSError*)> onError,
42
+ std::function<void()> onEnd, std::function<void()> onAbortDelivery = nullptr);
43
+ ~AVStreamSession();
44
+
45
+ AVStreamSession(const AVStreamSession&) = delete;
46
+ AVStreamSession& operator=(const AVStreamSession&) = delete;
47
+
48
+ // Resolves the display and starts the audio tap + both encoders; throws synchronously on setup
49
+ // failure (`onAccessUnit`/`onError`/`onEnd` never called then). Later failures from either
50
+ // encoder go to `onError`, then `onEnd` — mirrors VideoStreamSession's contract.
51
+ void Start();
52
+
53
+ // Idempotent; blocks until both encoders have fully stopped. Never call from inside
54
+ // onAccessUnit/onError/onEnd — same queues this blocks on, so it would deadlock.
55
+ void Stop();
56
+
57
+ // See VideoStreamSession::AbortDelivery — same contract, call before Stop() only when nothing
58
+ // else will drain the delivery queue concurrently.
59
+ void AbortDelivery();
60
+
61
+ // Forces the next encoded video frame to be a keyframe — see VideoFrameEncoder::RequestKeyFrame.
62
+ // No audio equivalent (every AAC-LC packet already self-decodable).
63
+ void RequestKeyFrame();
64
+
65
+ private:
66
+ class Impl;
67
+ std::unique_ptr<Impl> impl_;
68
+ };
69
+
70
+ } // namespace coresim