@ksp-gonogo/kerbcast 1.7.0 → 1.8.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 +39 -0
- package/dist/__generated__/types.d.ts +23 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/recording.d.ts +426 -0
- package/dist/recording.js +898 -0
- package/dist/testing/index.d.ts +88 -1
- package/dist/testing/index.js +200 -3
- package/package.json +4 -1
- package/src/__generated__/types.ts +21 -0
- package/src/client.test.ts +24 -0
- package/src/client.ts +74 -0
- package/src/index.ts +22 -0
- package/src/recording.test.ts +1471 -0
- package/src/recording.ts +1233 -0
- package/src/testing/index.ts +239 -1
- package/src/testing.test.ts +147 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { CameraState } from "./__generated__/types";
|
|
2
|
+
/**
|
|
3
|
+
* One point on a recording's UT track. `t` is milliseconds since recording
|
|
4
|
+
* start; `ut` is the KSP universal time (seconds) the capture clock reported
|
|
5
|
+
* at that instant, or absent when out of flight (no clock).
|
|
6
|
+
*/
|
|
7
|
+
export interface UtSample {
|
|
8
|
+
t: number;
|
|
9
|
+
ut?: number;
|
|
10
|
+
}
|
|
11
|
+
/** The finished, in-memory result of a single-feed recording. */
|
|
12
|
+
export interface RecordingHandle {
|
|
13
|
+
recordingId: string;
|
|
14
|
+
/** The camera this clip was recorded from. */
|
|
15
|
+
flightId: number;
|
|
16
|
+
/** The recorded media, in `mimeType`. */
|
|
17
|
+
blob: Blob;
|
|
18
|
+
/** The negotiated container/codec (e.g. `video/mp4;codecs=avc1`). */
|
|
19
|
+
mimeType: string;
|
|
20
|
+
/** UT (seconds) at recording start, or absent when out of flight. */
|
|
21
|
+
utStart?: number;
|
|
22
|
+
/** UT (seconds) at recording stop, or absent when out of flight. */
|
|
23
|
+
utEnd?: number;
|
|
24
|
+
/** ~1Hz (recording-time, UT) series across the clip. Always present. */
|
|
25
|
+
utSamples: UtSample[];
|
|
26
|
+
/** `blob.size` in bytes. */
|
|
27
|
+
byteSize: number;
|
|
28
|
+
/** Wall-clock recording duration in milliseconds. */
|
|
29
|
+
durationMs: number;
|
|
30
|
+
}
|
|
31
|
+
/** Options for {@link RecordingController.startRecording}. */
|
|
32
|
+
export interface StartRecordingOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Preferred container/codec. Tried first; if MediaRecorder can't support
|
|
35
|
+
* it, negotiation falls back (mp4 -> webm/vp9 -> webm). Omit to let
|
|
36
|
+
* negotiation choose.
|
|
37
|
+
*/
|
|
38
|
+
mimeType?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Auto-stop after this many milliseconds. The clip finalizes on its own and
|
|
41
|
+
* is retrievable via {@link RecordingController.fetchRecording}. Omit for
|
|
42
|
+
* manual stop only.
|
|
43
|
+
*/
|
|
44
|
+
maxDurationMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Force this feed to the operator's full render size for the duration of
|
|
47
|
+
* the recording (overrides the display-size demand only; still yields to
|
|
48
|
+
* the adaptive framerate shed). Off by default. A single recording arms
|
|
49
|
+
* and waits (bounded) for the bump before the clip starts, so it opens on
|
|
50
|
+
* full-res frames; a grouped recording starts immediately and the lead-in
|
|
51
|
+
* is cut via the existing UT-window trim instead. Released automatically
|
|
52
|
+
* on stop, discard, or auto-stop.
|
|
53
|
+
*/
|
|
54
|
+
forceFullResolution?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Bound on the single-recording arm-and-wait: how long
|
|
58
|
+
* {@link RecordingController.startRecording} waits for a forced feed to
|
|
59
|
+
* reach full resolution before starting the recorder anyway. Never hangs a
|
|
60
|
+
* start indefinitely (feed out of flight, wedged, or the sidecar/plugin
|
|
61
|
+
* round trip taking longer than expected).
|
|
62
|
+
*/
|
|
63
|
+
export declare const ARM_TIMEOUT_MS = 3000;
|
|
64
|
+
/** Options for {@link RecordingController.stopGroupedRecording}. */
|
|
65
|
+
export interface StopGroupedRecordingOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Coverage guarantee: hold finalization until every feed's latest UT sample
|
|
68
|
+
* has passed this UT (seconds), so no feed falls short of the common window.
|
|
69
|
+
* Omit to stop all feeds at the current moment (best-effort).
|
|
70
|
+
*
|
|
71
|
+
* The guarantee only holds for members that are still recording when the
|
|
72
|
+
* target arrives. A member started with its own `maxDurationMs` can
|
|
73
|
+
* auto-stop before reaching `targetUt`; that member's `utEnd` then falls
|
|
74
|
+
* short, which silently narrows (or, if the ranges no longer overlap,
|
|
75
|
+
* clears) the resulting `commonUtWindow`. Pair a grouped `targetUt` with an
|
|
76
|
+
* unbounded (or generously long) per-member `maxDurationMs`, or none at all.
|
|
77
|
+
*/
|
|
78
|
+
targetUt?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Bound on how long to wait for `targetUt` coverage before giving up and
|
|
81
|
+
* finalizing with whatever each feed has captured so far. Guards against a
|
|
82
|
+
* UT that never arrives (feed out of flight, sim frozen, warp reversed)
|
|
83
|
+
* hanging the stop forever. Ignored when `targetUt` is omitted. Defaults to
|
|
84
|
+
* {@link DEFAULT_COVERAGE_TIMEOUT_MS}.
|
|
85
|
+
*/
|
|
86
|
+
coverageTimeoutMs?: number;
|
|
87
|
+
}
|
|
88
|
+
/** Default {@link StopGroupedRecordingOptions.coverageTimeoutMs}. */
|
|
89
|
+
export declare const DEFAULT_COVERAGE_TIMEOUT_MS = 10000;
|
|
90
|
+
/** The result of {@link RecordingController.startGroupedRecording}. */
|
|
91
|
+
export interface GroupedRecordingStart {
|
|
92
|
+
groupId: string;
|
|
93
|
+
/**
|
|
94
|
+
* The real per-feed recording ids the controller minted internally, in the
|
|
95
|
+
* order `flightIds` was given. These are the ids that work with the
|
|
96
|
+
* single-recording surface (`isRecording`, `stopRecording`,
|
|
97
|
+
* `discardRecording`) for cancelling one member of the group.
|
|
98
|
+
*/
|
|
99
|
+
recordingIds: string[];
|
|
100
|
+
}
|
|
101
|
+
/** The finished, in-memory result of a grouped (multi-feed) recording. */
|
|
102
|
+
export interface GroupedRecordingHandle {
|
|
103
|
+
groupId: string;
|
|
104
|
+
/** One handle per feed, in the order the group was started. */
|
|
105
|
+
recordings: RecordingHandle[];
|
|
106
|
+
/**
|
|
107
|
+
* The UT span all feeds cover: [max(utStart), min(utEnd)] across the clips.
|
|
108
|
+
* Absent when any clip lacks UT or the ranges don't overlap. Each clip's
|
|
109
|
+
* own `utSamples` let a consumer map this window to that clip's
|
|
110
|
+
* recording-time and trim/seek itself (metadata alignment; no physical cut).
|
|
111
|
+
*/
|
|
112
|
+
commonUtWindow?: [number, number];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* An in-progress recording, surfaced so the UI can draw a REC dot and an
|
|
116
|
+
* elapsed timer. `groupId` is set when the recording is part of a grouped set.
|
|
117
|
+
*/
|
|
118
|
+
export interface ActiveRecordingInfo {
|
|
119
|
+
recordingId: string;
|
|
120
|
+
flightId: number;
|
|
121
|
+
/** `performance.now()`/`Date.now()` at start, for an elapsed timer. */
|
|
122
|
+
startedAt: number;
|
|
123
|
+
groupId?: string;
|
|
124
|
+
/**
|
|
125
|
+
* True while a single forced recording is armed (force sent, waiting for
|
|
126
|
+
* the feed to reach full resolution) but the recorder has not started
|
|
127
|
+
* yet. Always false for a non-forced recording and for grouped members
|
|
128
|
+
* (which record immediately). A UI shows a distinct "arming" state while
|
|
129
|
+
* this is true.
|
|
130
|
+
*/
|
|
131
|
+
arming: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Everything {@link RecordingController} currently knows, as one referentially
|
|
135
|
+
* stable object: the same reference between mutations, a new one after each.
|
|
136
|
+
* That stability is what makes it safe to hand straight to
|
|
137
|
+
* `useSyncExternalStore` -- a fresh object on every call would spin components
|
|
138
|
+
* into an infinite render loop.
|
|
139
|
+
*/
|
|
140
|
+
export interface RecordingsSnapshot {
|
|
141
|
+
/** Finished standalone (non-grouped) clips, newest last. */
|
|
142
|
+
recordings: RecordingHandle[];
|
|
143
|
+
/** Finished grouped sets, newest last. */
|
|
144
|
+
groups: GroupedRecordingHandle[];
|
|
145
|
+
/** Recordings currently in progress (single + grouped members). */
|
|
146
|
+
active: ActiveRecordingInfo[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Per-camera surface {@link RecordingController} needs: the live stream plus
|
|
150
|
+
* enough of the full camera handle to drive a forced full-resolution
|
|
151
|
+
* recording (send the force, observe the resulting render size).
|
|
152
|
+
*/
|
|
153
|
+
export interface RecordingClientCamera {
|
|
154
|
+
readonly mediaStream: MediaStream | null;
|
|
155
|
+
readonly state: CameraState | null;
|
|
156
|
+
/**
|
|
157
|
+
* The feed's physical/ceiling maximum render size, or `null` when not
|
|
158
|
+
* known to the client (e.g. `discover()` was never called). When known,
|
|
159
|
+
* lets the controller recognise a forced feed that has actually reached
|
|
160
|
+
* its ceiling rather than one merely unchanged since before the force.
|
|
161
|
+
*/
|
|
162
|
+
readonly maxRenderSize: {
|
|
163
|
+
width: number;
|
|
164
|
+
height: number;
|
|
165
|
+
} | null;
|
|
166
|
+
setForceFullResolution(force: boolean): Promise<void>;
|
|
167
|
+
on(event: "change", handler: (state: CameraState) => void): () => void;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Minimal surface {@link RecordingController} needs from the client. The full
|
|
171
|
+
* `KerbcastClient` satisfies it; a narrow interface keeps the controller
|
|
172
|
+
* unit-testable without a whole client.
|
|
173
|
+
*/
|
|
174
|
+
export interface RecordingClient {
|
|
175
|
+
camera(flightId: number): RecordingClientCamera;
|
|
176
|
+
readonly clock: {
|
|
177
|
+
readonly captureUt: number | null;
|
|
178
|
+
};
|
|
179
|
+
on(event: "settings-change", handler: (data: unknown) => void): () => void;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pick a MediaRecorder mime type. Tries `preferred` first (when given), then
|
|
183
|
+
* the standard preference order, using `MediaRecorder.isTypeSupported`. Falls
|
|
184
|
+
* back to `video/webm` when nothing reports support (or the API is absent).
|
|
185
|
+
*/
|
|
186
|
+
export declare function negotiateMimeType(preferred?: string): string;
|
|
187
|
+
/**
|
|
188
|
+
* Intersection of the feeds' UT ranges: [max(utStart), min(utEnd)]. Returns
|
|
189
|
+
* undefined if any clip lacks a full range or the spans don't overlap.
|
|
190
|
+
*/
|
|
191
|
+
export declare function commonUtWindow(recordings: RecordingHandle[]): [number, number] | undefined;
|
|
192
|
+
/**
|
|
193
|
+
* Recording-time (ms since clip start) for a mission UT, linearly interpolated
|
|
194
|
+
* off a clip's `utSamples`. UT is linear in recording time at constant
|
|
195
|
+
* time-warp (the launch/landing/docking moments a group captures), so this maps
|
|
196
|
+
* the common UT window onto each clip's own timeline for a keyframe trim.
|
|
197
|
+
* Clamps to the sampled span; returns null when the clip has no UT samples.
|
|
198
|
+
*/
|
|
199
|
+
export declare function utToRecordingTimeMs(samples: UtSample[], ut: number): number | null;
|
|
200
|
+
/**
|
|
201
|
+
* Remux-trims a grouped clip to a recording-time window. Implemented by a
|
|
202
|
+
* lazily-loaded package (Mediabunny) so single recordings and non-recording
|
|
203
|
+
* consumers never pull it. Keyframe-boundary remux only: no decode/re-encode.
|
|
204
|
+
*/
|
|
205
|
+
export interface GroupTrimmer {
|
|
206
|
+
/** Cut `blob` to [startMs, endMs] of its own timeline; resolves a new Blob. */
|
|
207
|
+
trim(blob: Blob, mimeType: string, startMs: number, endMs: number): Promise<Blob>;
|
|
208
|
+
}
|
|
209
|
+
/** Loads a {@link GroupTrimmer}, or resolves null when trimming is unavailable. */
|
|
210
|
+
export type TrimmerLoader = () => Promise<GroupTrimmer | null>;
|
|
211
|
+
/** Options for {@link RecordingController}. */
|
|
212
|
+
export interface RecordingControllerOptions {
|
|
213
|
+
/**
|
|
214
|
+
* Override the grouped-recording trimmer loader (tests inject a fake; a
|
|
215
|
+
* consumer could supply mp4box.js instead). Defaults to a dynamic
|
|
216
|
+
* `import("mediabunny")` keyframe remux.
|
|
217
|
+
*/
|
|
218
|
+
loadTrimmer?: TrimmerLoader;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Builds a {@link TrimmerLoader} around a caller-supplied module import.
|
|
222
|
+
* `loadMediabunnyTrimmer` (the default, below) resolves the bare
|
|
223
|
+
* `import("mediabunny")` specifier for bundler consumers (e.g. gonogo). A
|
|
224
|
+
* consumer that can't rely on that resolution -- the sidecar's embedded web
|
|
225
|
+
* page, which serves its own copy of the package locally for an offline/LAN
|
|
226
|
+
* install with no CDN -- supplies a different `importModule` (e.g. a dynamic
|
|
227
|
+
* import of a served URL) and passes the result as
|
|
228
|
+
* {@link RecordingControllerOptions.loadTrimmer}. Resolves null on any
|
|
229
|
+
* failure, so a group degrades to metadata-only rather than failing.
|
|
230
|
+
*/
|
|
231
|
+
export declare function createMediabunnyTrimmerLoader(importModule: () => Promise<unknown>): TrimmerLoader;
|
|
232
|
+
/**
|
|
233
|
+
* Owns every in-flight and finished recording for one client. Reached via
|
|
234
|
+
* `client.recording`. All state is in-memory: a page reload discards it.
|
|
235
|
+
*
|
|
236
|
+
* Observable: {@link subscribe} + {@link getSnapshot} let every call site
|
|
237
|
+
* (a tile's own REC control, a RecordingsTray mounted elsewhere) read the same
|
|
238
|
+
* state, so a clip started or finished through one is visible to all the
|
|
239
|
+
* others -- no per-instance state to fall out of sync.
|
|
240
|
+
*/
|
|
241
|
+
export declare class RecordingController {
|
|
242
|
+
private readonly client;
|
|
243
|
+
private readonly active;
|
|
244
|
+
private readonly finished;
|
|
245
|
+
/** flightId -> recordingId, so a second start on a busy feed is rejected. */
|
|
246
|
+
private readonly byFlight;
|
|
247
|
+
private readonly groups;
|
|
248
|
+
private readonly finishedGroups;
|
|
249
|
+
/** Per-group trimmer load kicked off at group-start, awaited at stop. */
|
|
250
|
+
private readonly groupTrimmers;
|
|
251
|
+
private readonly loadTrimmer;
|
|
252
|
+
/** recordingIds in a grouped set, so they stay out of the standalone `recordings` list. */
|
|
253
|
+
private readonly groupMemberIds;
|
|
254
|
+
/** recordingId -> groupId, so discarding one member can pull it out of its group's array. */
|
|
255
|
+
private readonly memberOfGroup;
|
|
256
|
+
/**
|
|
257
|
+
* Per-feed force ref-count. The sidecar OR-aggregates force across
|
|
258
|
+
* viewers already; this is the local mirror so two overlapping
|
|
259
|
+
* recordings of the same feed (in this one controller) release force
|
|
260
|
+
* only when the last of them stops.
|
|
261
|
+
*/
|
|
262
|
+
private readonly forceCounts;
|
|
263
|
+
/**
|
|
264
|
+
* Per-feed force-true send status: "pending" while a send is in flight,
|
|
265
|
+
* "sent" once it has actually succeeded. Absent means neither: either
|
|
266
|
+
* never attempted, or a prior attempt failed. Distinct from
|
|
267
|
+
* {@link forceCounts} so a failed send never masquerades as a completed
|
|
268
|
+
* one; see {@link acquireForce}.
|
|
269
|
+
*/
|
|
270
|
+
private readonly forceSendState;
|
|
271
|
+
private readonly listeners;
|
|
272
|
+
private snapshot;
|
|
273
|
+
/** While true, {@link rebuild} is a no-op; used to batch startGroupedRecording's
|
|
274
|
+
* per-member starts into a single notification once the group is fully set up. */
|
|
275
|
+
private suppressRebuild;
|
|
276
|
+
constructor(client: RecordingClient, options?: RecordingControllerOptions);
|
|
277
|
+
/** Subscribe to snapshot changes. Returns an unsubscribe function. */
|
|
278
|
+
subscribe: (fn: () => void) => (() => void);
|
|
279
|
+
/** The current snapshot; the same object reference until the next mutation. */
|
|
280
|
+
getSnapshot: () => RecordingsSnapshot;
|
|
281
|
+
/**
|
|
282
|
+
* Recompute the snapshot from current state and notify subscribers. Called
|
|
283
|
+
* at the end of every state-mutating operation, including the async settle
|
|
284
|
+
* points (recorder stop, grouped stop) where the data actually changes.
|
|
285
|
+
*/
|
|
286
|
+
private rebuild;
|
|
287
|
+
/**
|
|
288
|
+
* Acquire the local force ref-count for a feed; sends
|
|
289
|
+
* `setForceFullResolution(true)` unless one is already in flight or has
|
|
290
|
+
* already succeeded for this feed (tracked in {@link forceSendState}, not
|
|
291
|
+
* just the ref-count reaching 1). The sidecar OR-aggregates force across
|
|
292
|
+
* viewers already; this ref-count is the belt-and-braces local mirror for
|
|
293
|
+
* two overlapping recordings of the same feed in this one controller.
|
|
294
|
+
*
|
|
295
|
+
* A send failure clears `forceSendState` instead of leaving it "sent", so
|
|
296
|
+
* it never masquerades as done: a later acquire (e.g. a second overlapping
|
|
297
|
+
* recording's 1->2 transition) sees the feed as not-yet-forced and retries
|
|
298
|
+
* the send, rather than silently leaving the feed un-forced forever.
|
|
299
|
+
*/
|
|
300
|
+
private acquireForce;
|
|
301
|
+
/**
|
|
302
|
+
* Release one holder of a feed's force ref-count; sends
|
|
303
|
+
* `setForceFullResolution(false)` once the count reaches zero. Never goes
|
|
304
|
+
* below zero (a stray extra release is a no-op, not an underflow). Clears
|
|
305
|
+
* `forceSendState` too, so a later, unrelated recording of this feed
|
|
306
|
+
* starts its own send from a clean slate.
|
|
307
|
+
*/
|
|
308
|
+
private releaseForce;
|
|
309
|
+
/**
|
|
310
|
+
* Whether `state` shows a feed actually rendering at its full target size,
|
|
311
|
+
* as opposed to merely having caught up to whatever small size it already
|
|
312
|
+
* had. Two ways to know the target:
|
|
313
|
+
*
|
|
314
|
+
* - `target` known (from `maxRenderSize`): ready once the operator size
|
|
315
|
+
* has reached it and the rendered size has caught up to the operator
|
|
316
|
+
* size. Resolves promptly even when the feed was already at its ceiling
|
|
317
|
+
* before the force was sent (nothing ever "increases").
|
|
318
|
+
* - `target` unknown: falls back to requiring the operator size to have
|
|
319
|
+
* increased past `baseline` (the first REAL state observed at or after
|
|
320
|
+
* watch-start; see {@link watchResolutionReady}) and the rendered size
|
|
321
|
+
* to have caught up. `baseline` is `null` until that first real state
|
|
322
|
+
* arrives, and a `null` baseline is never ready: a placeholder `{0,0}`
|
|
323
|
+
* would let any positive operator size look like an increase, so no
|
|
324
|
+
* verdict is possible until a genuine baseline exists. This is a
|
|
325
|
+
* strictly weaker guarantee than the known-target path: a feed already
|
|
326
|
+
* at its true ceiling when forced will not be recognised as ready until
|
|
327
|
+
* something else changes its operator size, or the wait simply times
|
|
328
|
+
* out, but it still refuses to treat the pre-force snapshot itself as
|
|
329
|
+
* ready.
|
|
330
|
+
*
|
|
331
|
+
* Either way, a state whose rendered size has not yet caught up to its own
|
|
332
|
+
* operator size (adaptive shed still catching up) is never ready.
|
|
333
|
+
*/
|
|
334
|
+
private isAtFullResolution;
|
|
335
|
+
/**
|
|
336
|
+
* Seeds a fallback baseline from an observed state, or returns `null` if
|
|
337
|
+
* the state isn't a REAL observation (operator size still zero, i.e. no
|
|
338
|
+
* dimensions have ever been reported for this feed). A `null` result
|
|
339
|
+
* means the caller must keep waiting for a state worth basing readiness
|
|
340
|
+
* on, rather than latching onto a `{0,0}` placeholder.
|
|
341
|
+
*/
|
|
342
|
+
private seedResolutionBaseline;
|
|
343
|
+
/**
|
|
344
|
+
* Waits for a forced feed to actually render at full resolution.
|
|
345
|
+
* `timeoutMs` omitted means no bound: the wait only ends when the feed
|
|
346
|
+
* becomes ready or the caller cancels it (the grouped path, bounded by the
|
|
347
|
+
* recording's own lifetime instead; see {@link startRecording}).
|
|
348
|
+
* Resolves `true` once ready, `false` on timeout. Returns a `cancel` next
|
|
349
|
+
* to the promise so a caller can abandon the wait (stop/discard) without
|
|
350
|
+
* leaking the underlying "change" subscription or timer; cancelling never
|
|
351
|
+
* settles the promise.
|
|
352
|
+
*/
|
|
353
|
+
private watchResolutionReady;
|
|
354
|
+
/**
|
|
355
|
+
* Start recording a feed's live track. Returns a client-minted recordingId
|
|
356
|
+
* synchronously. Throws if the feed has no live track or is already
|
|
357
|
+
* recording.
|
|
358
|
+
*
|
|
359
|
+
* `inGroup` is internal (set by {@link startGroupedRecording}) and governs
|
|
360
|
+
* how `opts.forceFullResolution` behaves: a single recording (`inGroup`
|
|
361
|
+
* false) arms and waits, bounded by {@link ARM_TIMEOUT_MS}, for the feed to
|
|
362
|
+
* reach full resolution before the recorder actually starts, so the clip
|
|
363
|
+
* opens on full-res frames. A grouped member starts immediately and
|
|
364
|
+
* instead records the moment it reaches full resolution
|
|
365
|
+
* (`resolutionReadyAt`/`resolutionReadyUt`) so {@link stopGroupedRecording}
|
|
366
|
+
* can fold it into the common trim window.
|
|
367
|
+
*/
|
|
368
|
+
startRecording(flightId: number, opts?: StartRecordingOptions, inGroup?: boolean): string;
|
|
369
|
+
/**
|
|
370
|
+
* Stop a recording and resolve its finished handle. Idempotent per id: a
|
|
371
|
+
* second call (or a race with maxDurationMs auto-stop) returns the same
|
|
372
|
+
* pending/settled result.
|
|
373
|
+
*/
|
|
374
|
+
stopRecording(recordingId: string): Promise<RecordingHandle>;
|
|
375
|
+
/** Return a finished recording's handle, or undefined if unknown/still active. */
|
|
376
|
+
fetchRecording(recordingId: string): RecordingHandle | undefined;
|
|
377
|
+
/** Whether a feed currently has a recording in progress. */
|
|
378
|
+
isRecording(flightId: number): boolean;
|
|
379
|
+
/**
|
|
380
|
+
* Drop a recording. If still active, tears it down without producing a
|
|
381
|
+
* handle; if finished, frees the stored handle/blob reference. If the id
|
|
382
|
+
* is a member of a still-active group, pulls it out of that group's member
|
|
383
|
+
* set too, so a later `stopGroupedRecording` only waits on (and stops) the
|
|
384
|
+
* ids that remain -- discarding one member no longer corrupts the group.
|
|
385
|
+
*/
|
|
386
|
+
discardRecording(recordingId: string): void;
|
|
387
|
+
/**
|
|
388
|
+
* Start recording several feeds as one synchronised moment. All recorders
|
|
389
|
+
* open in a single tick to minimise skew. Returns the groupId plus the real
|
|
390
|
+
* per-feed recordingIds (so a caller can cancel one member individually via
|
|
391
|
+
* `stopRecording`/`discardRecording`, the same ids that end up on each
|
|
392
|
+
* clip's `RecordingHandle`). If any feed can't start (no track / already
|
|
393
|
+
* recording), the ones already started are discarded and the error
|
|
394
|
+
* rethrows.
|
|
395
|
+
*/
|
|
396
|
+
startGroupedRecording(flightIds: number[], opts?: StartRecordingOptions): GroupedRecordingStart;
|
|
397
|
+
/**
|
|
398
|
+
* Stop a grouped recording. With `targetUt`, holds until every feed's latest
|
|
399
|
+
* UT sample has passed it (coverage guarantee) before stopping any feed;
|
|
400
|
+
* without it, stops all now. Resolves the grouped handle with the intersected
|
|
401
|
+
* `commonUtWindow`. See {@link StopGroupedRecordingOptions.targetUt} for the
|
|
402
|
+
* coverage caveat around a member's own `maxDurationMs`.
|
|
403
|
+
*/
|
|
404
|
+
stopGroupedRecording(groupId: string, opts?: StopGroupedRecordingOptions): Promise<GroupedRecordingHandle>;
|
|
405
|
+
/**
|
|
406
|
+
* Remux-trim one clip to the common UT window, or return it untouched
|
|
407
|
+
* (metadata-only) when it can't be cut cleanly: a non-mp4 container, an
|
|
408
|
+
* unmappable window, or a trimmer error. The stored per-clip handle is
|
|
409
|
+
* replaced with the trimmed one so fetch/discard stay consistent.
|
|
410
|
+
*/
|
|
411
|
+
private maybeTrim;
|
|
412
|
+
/** Return a finished grouped recording, or undefined if unknown/still active. */
|
|
413
|
+
fetchGroupedRecording(groupId: string): GroupedRecordingHandle | undefined;
|
|
414
|
+
/** Drop a grouped recording and each of its clips. */
|
|
415
|
+
discardGroupedRecording(groupId: string): void;
|
|
416
|
+
/**
|
|
417
|
+
* Resolve once every listed recording's latest UT sample has reached
|
|
418
|
+
* `targetUt`. UT arrives on the shared ~1Hz clock feed, so this polls the
|
|
419
|
+
* settings-change stream rather than a wall-clock timer. Bounded by
|
|
420
|
+
* `timeoutMs`: if coverage never arrives (feed out of flight, sim frozen,
|
|
421
|
+
* warp reversed), gives up and resolves anyway so the group finalizes with
|
|
422
|
+
* whatever UT each feed has captured, rather than hanging forever. Always
|
|
423
|
+
* releases the settings-change subscription, on either exit path.
|
|
424
|
+
*/
|
|
425
|
+
private awaitCoverage;
|
|
426
|
+
}
|