@ksp-gonogo/kerbcast 1.6.3 → 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 +61 -0
- package/README.md +16 -0
- package/dist/__generated__/types.d.ts +129 -2
- package/dist/__generated__/types.js +31 -0
- package/dist/client.d.ts +74 -1
- package/dist/client.js +64 -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 +106 -2
- package/dist/testing/index.js +234 -4
- package/package.json +4 -1
- package/src/__generated__/types.ts +126 -2
- package/src/client.test.ts +63 -0
- package/src/client.ts +98 -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 +286 -2
- package/src/testing.test.ts +190 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased (next minor)
|
|
4
|
+
|
|
5
|
+
Client-side camera recording (pure SDK; no sidecar/plugin/protocol change).
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `KerbcastClient.recording`: a `RecordingController` that records a camera's
|
|
10
|
+
already-received WebRTC track with the browser's `MediaRecorder`, entirely
|
|
11
|
+
client-local (the sidecar never learns a recording happened)
|
|
12
|
+
- Per-feed API: `startRecording(flightId, opts?)` returns a client-minted
|
|
13
|
+
`recordingId`; `stopRecording(recordingId)` resolves a `RecordingHandle`;
|
|
14
|
+
`fetchRecording` / `discardRecording` / `isRecording`
|
|
15
|
+
- `RecordingHandle = { recordingId, blob, mimeType, utStart?, utEnd?,
|
|
16
|
+
utSamples, byteSize, durationMs }`, where `utSamples` is a ~1Hz
|
|
17
|
+
(recording-time, UT) series so a consumer can interpolate any frame's
|
|
18
|
+
mission-time near-frame-tight; out of flight it falls back to wall-clock
|
|
19
|
+
timing (`ut` absent) and upgrades automatically once UT appears
|
|
20
|
+
- Mime negotiation prefers `video/mp4;codecs=avc1` where
|
|
21
|
+
`MediaRecorder.isTypeSupported`, else `video/webm;codecs=vp9`, else
|
|
22
|
+
`video/webm`; the chosen type is on the handle. `opts.maxDurationMs` auto-stops
|
|
23
|
+
- Grouped API: `startGroupedRecording(flightIds, opts?)` (all recorders open in
|
|
24
|
+
one tick) and `stopGroupedRecording(groupId, { targetUt? })`. `targetUt`
|
|
25
|
+
enforces a coverage guarantee: finalization waits until every feed's latest
|
|
26
|
+
UT sample passes it, so no feed falls short of the common window
|
|
27
|
+
- `GroupedRecordingHandle = { groupId, recordings, commonUtWindow? }`, where
|
|
28
|
+
`commonUtWindow` is the intersection of the feeds' `[utStart, utEnd]`
|
|
29
|
+
- Grouped clips are physically remux-trimmed to `commonUtWindow` by default
|
|
30
|
+
(keyframe-boundary cut, no re-encode) via a lazily `import()`-ed Mediabunny,
|
|
31
|
+
loaded only on grouped-record start (single recordings stay dependency-free);
|
|
32
|
+
a clip that can't be cut cleanly (webm, or a remux error) degrades to
|
|
33
|
+
metadata-only and never fails the recording. `utSamples` + `commonUtWindow`
|
|
34
|
+
still ride along after trimming so a consumer can align to telemetry
|
|
35
|
+
- Helpers `negotiateMimeType`, `commonUtWindow`, `utToRecordingTimeMs`
|
|
36
|
+
- `StubMediaRecorder`: a jsdom `MediaRecorder` shim installed by
|
|
37
|
+
`installDomStubs`, so the full record -> UT-sample -> stop -> handle path is
|
|
38
|
+
headless-testable (its `isTypeSupported` is controllable for mime tests)
|
|
39
|
+
- `mediabunny` as a dependency, dynamically imported for grouped-clip trimming
|
|
40
|
+
only (never pulled by single recordings or non-recording consumers)
|
|
41
|
+
|
|
42
|
+
## 1.7.0 - 2026-07-24
|
|
43
|
+
|
|
44
|
+
Crew face cameras, auto-resolution, and camera targeting.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `KerbcastClient.reportDisplaySize(flightId, width, height)`: report a
|
|
49
|
+
consumer's own displayed pixel size for a camera. The sidecar aggregates it
|
|
50
|
+
MAX-across-consumers to drive auto-resolution (distinct from `setRenderSize`,
|
|
51
|
+
which is the shared operator cap).
|
|
52
|
+
- `KerbcastClient.setTrackTarget(flightId, mode)`: ask a pan+zoom camera to
|
|
53
|
+
auto-track a moving vessel, or stop.
|
|
54
|
+
- `TrackMode` type (`"none" | "activeVessel" | "target"`).
|
|
55
|
+
- `trackMode?: TrackMode` on `CameraState` (WS push) and `CameraInfo`
|
|
56
|
+
(`GET /cameras`), server-authoritative so every consumer reflects the same
|
|
57
|
+
tracking state.
|
|
58
|
+
- `MockSidecar` accepts the new `report-display-size` and `set-track-target`
|
|
59
|
+
client messages (records them; `report-display-size` is advisory and does not
|
|
60
|
+
mutate render dims).
|
|
61
|
+
- `MockCameraInit` gains `kind`, `crewLocation`, `kerbalPersistentId`, and
|
|
62
|
+
`trackMode` for building crew and tracking fixtures.
|
|
63
|
+
|
|
3
64
|
## 0.12.0 - 2026-06-07
|
|
4
65
|
|
|
5
66
|
### Added
|
package/README.md
CHANGED
|
@@ -106,6 +106,22 @@ controls each part actually supports.
|
|
|
106
106
|
skip, producing the macroblocking and stuttering aesthetic of
|
|
107
107
|
in-game CommNet signal loss. Also a real CPU optimisation when
|
|
108
108
|
every consumer wants degraded video.
|
|
109
|
+
- `kind` (`"part"` | `"kerbal"`) and, for a kerbal face camera,
|
|
110
|
+
`crewLocation` (`"seat"` | `"eva"`). Distinguish crew face cameras
|
|
111
|
+
from part cameras.
|
|
112
|
+
- `trackMode` (`"none"` | `"activeVessel"` | `"target"`). Server-authoritative
|
|
113
|
+
auto-track state on a pan+zoom camera, so every consumer reflects the same
|
|
114
|
+
tracking state.
|
|
115
|
+
|
|
116
|
+
## Auto-resolution and targeting
|
|
117
|
+
|
|
118
|
+
- `client.reportDisplaySize(flightId, width, height)`. Report a consumer's
|
|
119
|
+
displayed pixel size for a camera; the sidecar takes the MAX across consumers
|
|
120
|
+
to size the encode (distinct from `setRenderSize`, the shared operator cap).
|
|
121
|
+
The react feed primitives self-measure and call this for you.
|
|
122
|
+
- `client.setTrackTarget(flightId, mode)`. Ask a pan+zoom camera to auto-track
|
|
123
|
+
the active vessel or its target (`TrackMode`), or `"none"` to stop. The
|
|
124
|
+
chosen mode is published back on `CameraState.trackMode`.
|
|
109
125
|
|
|
110
126
|
## Pan/zoom input helper
|
|
111
127
|
|
|
@@ -14,6 +14,34 @@ export declare enum CameraLifecycle {
|
|
|
14
14
|
Active = "active",
|
|
15
15
|
Destroyed = "destroyed"
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Distinguishes an existing Hullcam part camera (`Part`) from a
|
|
19
|
+
* per-kerbal face camera (`Kerbal`).
|
|
20
|
+
*/
|
|
21
|
+
export declare enum CameraKind {
|
|
22
|
+
Part = "part",
|
|
23
|
+
Kerbal = "kerbal"
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Which source a kerbal camera is currently rendering: a seated IVA
|
|
27
|
+
* portrait, or the kerbal's own view while on EVA. Only meaningful when
|
|
28
|
+
* `CameraState.kind == Kerbal`.
|
|
29
|
+
*/
|
|
30
|
+
export declare enum CrewLocation {
|
|
31
|
+
Seat = "seat",
|
|
32
|
+
Eva = "eva"
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Which moving thing a pan+zoom camera should auto-aim at. `None` = tracking
|
|
36
|
+
* off (manual pan resumes). `ActiveVessel` and `Target` resolve to a world
|
|
37
|
+
* position from `FlightGlobals` (the active vessel, or its target) with no
|
|
38
|
+
* orbit math. Only surfaces on cameras with BOTH pan and zoom.
|
|
39
|
+
*/
|
|
40
|
+
export declare enum TrackMode {
|
|
41
|
+
None = "none",
|
|
42
|
+
ActiveVessel = "activeVessel",
|
|
43
|
+
Target = "target"
|
|
44
|
+
}
|
|
17
45
|
/**
|
|
18
46
|
* Layer mask. Mirrors `Kerbcast.CameraLayers` on the plugin side.
|
|
19
47
|
* Receiving clients use this for both the rendered-layer status reports
|
|
@@ -62,6 +90,29 @@ export interface CameraState {
|
|
|
62
90
|
* surface a "SIGNAL LOST" overlay and keep the last frame visible.
|
|
63
91
|
*/
|
|
64
92
|
lifecycle?: CameraLifecycle;
|
|
93
|
+
/**
|
|
94
|
+
* Whether this is a Hullcam part camera or a per-kerbal face camera.
|
|
95
|
+
* Defaults to `Part` so existing payloads are unaffected.
|
|
96
|
+
*/
|
|
97
|
+
kind?: CameraKind;
|
|
98
|
+
/**
|
|
99
|
+
* The kerbal's real `ProtoCrewMember.persistentID`: the stable key a
|
|
100
|
+
* consumer correlates against. `None` for part cameras. Distinct from
|
|
101
|
+
* `flight_id`, which identifies the camera instance, not the kerbal.
|
|
102
|
+
*/
|
|
103
|
+
kerbalPersistentId?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Which source a kerbal camera is currently rendering. `None` for
|
|
106
|
+
* part cameras.
|
|
107
|
+
*/
|
|
108
|
+
crewLocation?: CrewLocation;
|
|
109
|
+
/**
|
|
110
|
+
* Server-authoritative auto-track state for a pan+zoom camera (the
|
|
111
|
+
* operator's persistent intent). `None` (the default) = not tracking.
|
|
112
|
+
* Every browser reflects this; a browser track overrides a kOS aim on the
|
|
113
|
+
* same camera (send `SetTrackTarget` with `none` to hand back to kOS).
|
|
114
|
+
*/
|
|
115
|
+
trackMode?: TrackMode;
|
|
65
116
|
partName: string;
|
|
66
117
|
partTitle: string;
|
|
67
118
|
cameraName: string;
|
|
@@ -175,6 +226,19 @@ export interface HelloPayload {
|
|
|
175
226
|
sidecarVersion: string;
|
|
176
227
|
encoderBackend: string;
|
|
177
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* A consumer reporting its OWN current display size in px for one camera.
|
|
231
|
+
* Unlike `SetRenderSizePayload` (an operator command that sets the shared
|
|
232
|
+
* render size directly, last-writer-wins), this is a per-consumer input the
|
|
233
|
+
* sidecar aggregates MAX-across-consumers to drive auto-resolution
|
|
234
|
+
* (meet-the-minimum-need). Shape mirrors `SetRenderSizePayload`; `width` and
|
|
235
|
+
* `height` are the consumer's display px for this feed.
|
|
236
|
+
*/
|
|
237
|
+
export interface ReportDisplaySizePayload {
|
|
238
|
+
flightId: number;
|
|
239
|
+
width: number;
|
|
240
|
+
height: number;
|
|
241
|
+
}
|
|
178
242
|
/**
|
|
179
243
|
* Scene-state change: whether KSP is currently in a flight scene. Sent
|
|
180
244
|
* after `Hello` (priming) and whenever the polled in-flight flag flips,
|
|
@@ -196,6 +260,17 @@ export interface SetDegradePayload {
|
|
|
196
260
|
*/
|
|
197
261
|
level: number;
|
|
198
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Forces a camera to render at the operator ceiling regardless of
|
|
265
|
+
* display-size demand. Per-viewer, OR-aggregated by the sidecar: a feed
|
|
266
|
+
* stays forced while any connected viewer forces it, and releases when
|
|
267
|
+
* the last one clears it (or disconnects). Still yields to the adaptive
|
|
268
|
+
* framerate shed; overrides only the demand term.
|
|
269
|
+
*/
|
|
270
|
+
export interface SetForceFullResolutionPayload {
|
|
271
|
+
flightId: number;
|
|
272
|
+
force: boolean;
|
|
273
|
+
}
|
|
199
274
|
export interface SetFovPayload {
|
|
200
275
|
flightId: number;
|
|
201
276
|
fov: number;
|
|
@@ -248,6 +323,21 @@ export interface SetThrottleMainScreenPayload {
|
|
|
248
323
|
*/
|
|
249
324
|
enabled: boolean;
|
|
250
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Ask a pan+zoom camera to auto-track a moving vessel (or stop). Mirrors
|
|
328
|
+
* `SetRenderSizePayload` in shape. Server-authoritative: the sidecar holds the
|
|
329
|
+
* chosen mode per camera and publishes it in `CameraState.track_mode`, so
|
|
330
|
+
* every browser reflects the same tracking state (never optimistic-local).
|
|
331
|
+
*
|
|
332
|
+
* Precedence note (for operators): a browser track OVERRIDES a kOS aim on the
|
|
333
|
+
* same camera; set `mode: none` to hand aiming back to kOS. While tracking, the
|
|
334
|
+
* camera also AUTO-ZOOMS to keep the target framed (distance-driven), owning the
|
|
335
|
+
* zoom over any manual FoV; `mode: none` restores manual zoom untouched.
|
|
336
|
+
*/
|
|
337
|
+
export interface SetTrackTargetPayload {
|
|
338
|
+
flightId: number;
|
|
339
|
+
mode: TrackMode;
|
|
340
|
+
}
|
|
251
341
|
export interface SetZoomRatePayload {
|
|
252
342
|
flightId: number;
|
|
253
343
|
/**
|
|
@@ -324,13 +414,50 @@ export type ClientMessage =
|
|
|
324
414
|
content: SetLayersPayload;
|
|
325
415
|
}
|
|
326
416
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
417
|
+
* Cap the auto-resolution for one camera. As of Stage 6 this is a
|
|
418
|
+
* CEILING on the demand-driven size (effective = min(auto max-across-
|
|
419
|
+
* consumers, this cap)), NOT an authoritative set: it cannot force a
|
|
420
|
+
* camera larger than current consumers report, only smaller. Even
|
|
421
|
+
* pixels only (H.264 chroma); server caps at the ring's allocated max.
|
|
329
422
|
*/
|
|
330
423
|
| {
|
|
331
424
|
type: "set-render-size";
|
|
332
425
|
content: SetRenderSizePayload;
|
|
333
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Auto-track a moving vessel with a pan+zoom camera (or stop). The sidecar
|
|
429
|
+
* holds the chosen `mode` per camera and publishes it in
|
|
430
|
+
* `CameraState.track_mode`, so tracking state is server-authoritative and
|
|
431
|
+
* consistent across browsers. A browser track OVERRIDES a kOS aim on the
|
|
432
|
+
* same camera; send `mode: none` to hand aiming back to kOS.
|
|
433
|
+
*/
|
|
434
|
+
| {
|
|
435
|
+
type: "set-track-target";
|
|
436
|
+
content: SetTrackTargetPayload;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* A consumer reporting its OWN current display size in px; the sidecar
|
|
440
|
+
* aggregates MAX-across-consumers to drive auto-resolution
|
|
441
|
+
* (meet-the-minimum-need). Distinct from the operator `SetRenderSize`
|
|
442
|
+
* command: this is a per-consumer input, not a shared override — a
|
|
443
|
+
* departing consumer's size is cleared so the max relaxes.
|
|
444
|
+
*/
|
|
445
|
+
| {
|
|
446
|
+
type: "report-display-size";
|
|
447
|
+
content: ReportDisplaySizePayload;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Force a camera to render at the operator ceiling regardless of
|
|
451
|
+
* display-size demand. Per-viewer, OR-aggregated by the sidecar: a
|
|
452
|
+
* feed stays forced while any connected viewer forces it, and
|
|
453
|
+
* releases when the last one clears it (or disconnects). Still
|
|
454
|
+
* yields to the adaptive framerate shed; overrides only the demand
|
|
455
|
+
* term, never the perf-protection ceiling.
|
|
456
|
+
*/
|
|
457
|
+
| {
|
|
458
|
+
type: "set-force-full-resolution";
|
|
459
|
+
content: SetForceFullResolutionPayload;
|
|
460
|
+
}
|
|
334
461
|
/**
|
|
335
462
|
* Set the camera's field-of-view (degrees). Silently ignored for
|
|
336
463
|
* parts whose Hullcam module is the fixed base (`supportsZoom ==
|
|
@@ -13,6 +13,37 @@ export var CameraLifecycle;
|
|
|
13
13
|
CameraLifecycle["Active"] = "active";
|
|
14
14
|
CameraLifecycle["Destroyed"] = "destroyed";
|
|
15
15
|
})(CameraLifecycle || (CameraLifecycle = {}));
|
|
16
|
+
/**
|
|
17
|
+
* Distinguishes an existing Hullcam part camera (`Part`) from a
|
|
18
|
+
* per-kerbal face camera (`Kerbal`).
|
|
19
|
+
*/
|
|
20
|
+
export var CameraKind;
|
|
21
|
+
(function (CameraKind) {
|
|
22
|
+
CameraKind["Part"] = "part";
|
|
23
|
+
CameraKind["Kerbal"] = "kerbal";
|
|
24
|
+
})(CameraKind || (CameraKind = {}));
|
|
25
|
+
/**
|
|
26
|
+
* Which source a kerbal camera is currently rendering: a seated IVA
|
|
27
|
+
* portrait, or the kerbal's own view while on EVA. Only meaningful when
|
|
28
|
+
* `CameraState.kind == Kerbal`.
|
|
29
|
+
*/
|
|
30
|
+
export var CrewLocation;
|
|
31
|
+
(function (CrewLocation) {
|
|
32
|
+
CrewLocation["Seat"] = "seat";
|
|
33
|
+
CrewLocation["Eva"] = "eva";
|
|
34
|
+
})(CrewLocation || (CrewLocation = {}));
|
|
35
|
+
/**
|
|
36
|
+
* Which moving thing a pan+zoom camera should auto-aim at. `None` = tracking
|
|
37
|
+
* off (manual pan resumes). `ActiveVessel` and `Target` resolve to a world
|
|
38
|
+
* position from `FlightGlobals` (the active vessel, or its target) with no
|
|
39
|
+
* orbit math. Only surfaces on cameras with BOTH pan and zoom.
|
|
40
|
+
*/
|
|
41
|
+
export var TrackMode;
|
|
42
|
+
(function (TrackMode) {
|
|
43
|
+
TrackMode["None"] = "none";
|
|
44
|
+
TrackMode["ActiveVessel"] = "activeVessel";
|
|
45
|
+
TrackMode["Target"] = "target";
|
|
46
|
+
})(TrackMode || (TrackMode = {}));
|
|
16
47
|
/**
|
|
17
48
|
* Layer mask. Mirrors `Kerbcast.CameraLayers` on the plugin side.
|
|
18
49
|
* Receiving clients use this for both the rendered-layer status reports
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AdaptiveShedPayload, CameraState, ClientMessage, ErrorPayload, Layer, QualityPreset, SettingsStatePayload } from "./__generated__/types";
|
|
1
|
+
import type { AdaptiveShedPayload, CameraState, ClientMessage, ErrorPayload, Layer, QualityPreset, SettingsStatePayload, TrackMode } from "./__generated__/types";
|
|
2
2
|
import { CameraLifecycle } from "./__generated__/types";
|
|
3
|
+
import { RecordingController, type RecordingControllerOptions } from "./recording";
|
|
3
4
|
/** Controls the digital-static noise overlay baked into `cam.mediaStream`. */
|
|
4
5
|
export interface NoiseConfig {
|
|
5
6
|
/** When false, the pipeline is bypassed and `mediaStream` is the raw WebRTC track. */
|
|
@@ -22,6 +23,19 @@ export interface KerbcastCameraHandle {
|
|
|
22
23
|
readonly stalled: boolean;
|
|
23
24
|
/** Current {@link setShowStatic} setting. */
|
|
24
25
|
readonly showStatic: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The feed's physical/ceiling maximum render size (`DiscoveredCamera.maxWidth`
|
|
28
|
+
* / `maxHeight`), or `null` until known. Populated from a prior
|
|
29
|
+
* `client.discover()` call (there is no live-pushed field for it), so a
|
|
30
|
+
* consumer that never calls `discover()` sees `null` here indefinitely.
|
|
31
|
+
* Used by `RecordingController` to recognise a force-full-resolution feed
|
|
32
|
+
* that has actually reached its ceiling, as opposed to one merely
|
|
33
|
+
* unchanged since before the force was sent.
|
|
34
|
+
*/
|
|
35
|
+
readonly maxRenderSize: {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
} | null;
|
|
25
39
|
setLayers(layers: Layer[]): Promise<void>;
|
|
26
40
|
setRenderSize(width: number, height: number): Promise<void>;
|
|
27
41
|
setFov(fov: number): Promise<void>;
|
|
@@ -52,6 +66,15 @@ export interface KerbcastCameraHandle {
|
|
|
52
66
|
* machinery keeps overriding it downward while throttled.
|
|
53
67
|
*/
|
|
54
68
|
setQuality(preset: QualityPreset | null): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Force this feed to render at the operator's configured maximum
|
|
71
|
+
* regardless of display size, until released by calling this with
|
|
72
|
+
* `false`. Per-viewer, OR-aggregated server-side across viewers: a feed
|
|
73
|
+
* stays forced while any viewer forces it, and releases when the last one
|
|
74
|
+
* clears it (or disconnects). Still yields to the adaptive framerate
|
|
75
|
+
* shed; it only overrides the display-size demand term.
|
|
76
|
+
*/
|
|
77
|
+
setForceFullResolution(force: boolean): Promise<void>;
|
|
55
78
|
requestKeyframe(): Promise<void>;
|
|
56
79
|
/**
|
|
57
80
|
* Override noise settings for this camera only. Takes precedence over the
|
|
@@ -121,6 +144,15 @@ export interface KerbcastClientConfig {
|
|
|
121
144
|
sdp: string;
|
|
122
145
|
cameras: number[];
|
|
123
146
|
}>;
|
|
147
|
+
/**
|
|
148
|
+
* Options for the lazily-built {@link RecordingController} (`client.recording`).
|
|
149
|
+
* The load-bearing use is `loadTrimmer`: a consumer that can't rely on the
|
|
150
|
+
* default bare `import("mediabunny")` resolving (the sidecar's embedded web
|
|
151
|
+
* page serves its own copy of the package locally, offline/LAN, no CDN)
|
|
152
|
+
* supplies its own loader here. Bundler consumers (e.g. gonogo) can omit
|
|
153
|
+
* this and get the default.
|
|
154
|
+
*/
|
|
155
|
+
recording?: RecordingControllerOptions;
|
|
124
156
|
}
|
|
125
157
|
/** WebRTC connection state surface. */
|
|
126
158
|
export type KerbcastConnectionState = "disconnected" | "connecting" | "connected" | "failed";
|
|
@@ -328,6 +360,15 @@ export declare class KerbcastClient extends TypedEmitter<KerbcastClientEvents> {
|
|
|
328
360
|
private _warpRate;
|
|
329
361
|
/** Whether KSP is in a flight scene. `undefined` until the first signal. */
|
|
330
362
|
private _inFlight;
|
|
363
|
+
/** Client-side recording controller, lazily built on first `recording` access. */
|
|
364
|
+
private _recording;
|
|
365
|
+
/**
|
|
366
|
+
* Physical/ceiling max render size per camera, cached from the last
|
|
367
|
+
* `discover()` call. Empty until `discover()` has been called at least
|
|
368
|
+
* once; a camera absent from the last discovery response is simply
|
|
369
|
+
* missing here (its handle's `maxRenderSize` reads `null`).
|
|
370
|
+
*/
|
|
371
|
+
private readonly maxRenderSizeByFlight;
|
|
331
372
|
constructor(cfg: KerbcastClientConfig, transport?: KerbcastTransport);
|
|
332
373
|
get state(): KerbcastConnectionState;
|
|
333
374
|
get cameras(): ReadonlyArray<CameraState>;
|
|
@@ -352,6 +393,13 @@ export declare class KerbcastClient extends TypedEmitter<KerbcastClientEvents> {
|
|
|
352
393
|
epoch: number;
|
|
353
394
|
warpRate: number;
|
|
354
395
|
};
|
|
396
|
+
/**
|
|
397
|
+
* Client-side recording controller. Records a camera's already-received
|
|
398
|
+
* track with the browser's `MediaRecorder`, annotated with the mission-time
|
|
399
|
+
* (UT) clock; purely local, no sidecar involvement. Lazily created on first
|
|
400
|
+
* access and stable thereafter. See {@link RecordingController}.
|
|
401
|
+
*/
|
|
402
|
+
get recording(): RecordingController;
|
|
355
403
|
/**
|
|
356
404
|
* Whether KSP is currently in a flight scene, from the sidecar's
|
|
357
405
|
* `scene-state-changed`. `undefined` until the first signal (so a
|
|
@@ -371,6 +419,14 @@ export declare class KerbcastClient extends TypedEmitter<KerbcastClientEvents> {
|
|
|
371
419
|
* to subscribe to before opening a peer connection.
|
|
372
420
|
*/
|
|
373
421
|
discover(): Promise<DiscoveredCamera[]>;
|
|
422
|
+
/**
|
|
423
|
+
* Internal: backs {@link KerbcastCameraHandle.maxRenderSize}. `null` when
|
|
424
|
+
* `discover()` has never been called or never reported this camera.
|
|
425
|
+
*/
|
|
426
|
+
_maxRenderSize(flightId: number): {
|
|
427
|
+
width: number;
|
|
428
|
+
height: number;
|
|
429
|
+
} | null;
|
|
374
430
|
/**
|
|
375
431
|
* Open the WebRTC peer and subscribe to cameras. Idempotent: calling
|
|
376
432
|
* `connect` while already connected disconnects first.
|
|
@@ -409,6 +465,23 @@ export declare class KerbcastClient extends TypedEmitter<KerbcastClientEvents> {
|
|
|
409
465
|
* goes null once the sidecar confirms with a SlotMap.
|
|
410
466
|
*/
|
|
411
467
|
unsubscribe(flightId: number): Promise<void>;
|
|
468
|
+
/**
|
|
469
|
+
* Report this consumer's own displayed pixel size for a camera, driving the
|
|
470
|
+
* sidecar's auto-resolution. Unlike `setRenderSize` (an operator command that
|
|
471
|
+
* sets the shared render size directly, last-writer-wins), this is a
|
|
472
|
+
* per-consumer input the sidecar aggregates MAX-across-consumers, so a big
|
|
473
|
+
* spotlight bumps the stream up while tiny avatars leave it small. The SDK's
|
|
474
|
+
* feed primitives self-measure and call this; consumers do not report by hand.
|
|
475
|
+
*/
|
|
476
|
+
reportDisplaySize(flightId: number, width: number, height: number): Promise<void>;
|
|
477
|
+
/**
|
|
478
|
+
* Ask a pan+zoom camera to auto-track a moving vessel (or stop). Sends the
|
|
479
|
+
* intent; the sidecar holds the chosen mode per camera and publishes it in
|
|
480
|
+
* `CameraState.trackMode`, so the UI must reflect that server-confirmed value
|
|
481
|
+
* rather than this call (two browsers stay in agreement). While tracking, the
|
|
482
|
+
* camera also auto-zooms; `TrackMode.None` hands aiming/zoom back.
|
|
483
|
+
*/
|
|
484
|
+
setTrackTarget(flightId: number, mode: TrackMode): Promise<void>;
|
|
412
485
|
/**
|
|
413
486
|
* Internal — resolves the effective noise enabled state for a camera,
|
|
414
487
|
* merging the per-camera override (if any) with the client-level default.
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CameraLifecycle, ErrorSource } from "./__generated__/types";
|
|
2
2
|
import { tryCreateNoisePipeline } from "./noise";
|
|
3
|
+
import { RecordingController } from "./recording";
|
|
3
4
|
/** Intensity the static runs at when a camera has no live source (signal lost). */
|
|
4
5
|
const SOURCELESS_INTENSITY = 1.0;
|
|
5
6
|
/** Floor applied to the degrade-driven intensity of a live feed. */
|
|
@@ -185,6 +186,9 @@ class CameraHandle extends TypedEmitter {
|
|
|
185
186
|
get showStatic() {
|
|
186
187
|
return this._showStatic;
|
|
187
188
|
}
|
|
189
|
+
get maxRenderSize() {
|
|
190
|
+
return this.client._maxRenderSize(this.flightId);
|
|
191
|
+
}
|
|
188
192
|
configure(options) {
|
|
189
193
|
this._noiseOverride = options.noise ?? null;
|
|
190
194
|
// Re-evaluate with the new noise setting: rebuild from whatever source
|
|
@@ -390,6 +394,12 @@ class CameraHandle extends TypedEmitter {
|
|
|
390
394
|
content: { flightId: this.flightId, preset: preset ?? undefined },
|
|
391
395
|
});
|
|
392
396
|
}
|
|
397
|
+
async setForceFullResolution(force) {
|
|
398
|
+
await this.client._send({
|
|
399
|
+
type: "set-force-full-resolution",
|
|
400
|
+
content: { flightId: this.flightId, force },
|
|
401
|
+
});
|
|
402
|
+
}
|
|
393
403
|
async requestKeyframe() {
|
|
394
404
|
await this.client._send({
|
|
395
405
|
type: "request-keyframe",
|
|
@@ -465,6 +475,15 @@ export class KerbcastClient extends TypedEmitter {
|
|
|
465
475
|
this._warpRate = 1;
|
|
466
476
|
/** Whether KSP is in a flight scene. `undefined` until the first signal. */
|
|
467
477
|
this._inFlight = undefined;
|
|
478
|
+
/** Client-side recording controller, lazily built on first `recording` access. */
|
|
479
|
+
this._recording = null;
|
|
480
|
+
/**
|
|
481
|
+
* Physical/ceiling max render size per camera, cached from the last
|
|
482
|
+
* `discover()` call. Empty until `discover()` has been called at least
|
|
483
|
+
* once; a camera absent from the last discovery response is simply
|
|
484
|
+
* missing here (its handle's `maxRenderSize` reads `null`).
|
|
485
|
+
*/
|
|
486
|
+
this.maxRenderSizeByFlight = new Map();
|
|
468
487
|
this.cfg = cfg;
|
|
469
488
|
this.transport =
|
|
470
489
|
transport ??
|
|
@@ -507,6 +526,17 @@ export class KerbcastClient extends TypedEmitter {
|
|
|
507
526
|
warpRate: this._warpRate,
|
|
508
527
|
};
|
|
509
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Client-side recording controller. Records a camera's already-received
|
|
531
|
+
* track with the browser's `MediaRecorder`, annotated with the mission-time
|
|
532
|
+
* (UT) clock; purely local, no sidecar involvement. Lazily created on first
|
|
533
|
+
* access and stable thereafter. See {@link RecordingController}.
|
|
534
|
+
*/
|
|
535
|
+
get recording() {
|
|
536
|
+
if (!this._recording)
|
|
537
|
+
this._recording = new RecordingController(this, this.cfg.recording);
|
|
538
|
+
return this._recording;
|
|
539
|
+
}
|
|
510
540
|
/**
|
|
511
541
|
* Whether KSP is currently in a flight scene, from the sidecar's
|
|
512
542
|
* `scene-state-changed`. `undefined` until the first signal (so a
|
|
@@ -537,8 +567,21 @@ export class KerbcastClient extends TypedEmitter {
|
|
|
537
567
|
if (!res.ok)
|
|
538
568
|
throw new Error(`/cameras returned ${res.status}`);
|
|
539
569
|
const body = (await res.json());
|
|
570
|
+
for (const cam of body.cameras) {
|
|
571
|
+
this.maxRenderSizeByFlight.set(cam.flightId, {
|
|
572
|
+
width: cam.maxWidth,
|
|
573
|
+
height: cam.maxHeight,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
540
576
|
return body.cameras;
|
|
541
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Internal: backs {@link KerbcastCameraHandle.maxRenderSize}. `null` when
|
|
580
|
+
* `discover()` has never been called or never reported this camera.
|
|
581
|
+
*/
|
|
582
|
+
_maxRenderSize(flightId) {
|
|
583
|
+
return this.maxRenderSizeByFlight.get(flightId) ?? null;
|
|
584
|
+
}
|
|
542
585
|
/**
|
|
543
586
|
* Open the WebRTC peer and subscribe to cameras. Idempotent: calling
|
|
544
587
|
* `connect` while already connected disconnects first.
|
|
@@ -658,6 +701,27 @@ export class KerbcastClient extends TypedEmitter {
|
|
|
658
701
|
async unsubscribe(flightId) {
|
|
659
702
|
await this._send({ type: "unsubscribe", content: { flightId } });
|
|
660
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Report this consumer's own displayed pixel size for a camera, driving the
|
|
706
|
+
* sidecar's auto-resolution. Unlike `setRenderSize` (an operator command that
|
|
707
|
+
* sets the shared render size directly, last-writer-wins), this is a
|
|
708
|
+
* per-consumer input the sidecar aggregates MAX-across-consumers, so a big
|
|
709
|
+
* spotlight bumps the stream up while tiny avatars leave it small. The SDK's
|
|
710
|
+
* feed primitives self-measure and call this; consumers do not report by hand.
|
|
711
|
+
*/
|
|
712
|
+
async reportDisplaySize(flightId, width, height) {
|
|
713
|
+
await this._send({ type: "report-display-size", content: { flightId, width, height } });
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Ask a pan+zoom camera to auto-track a moving vessel (or stop). Sends the
|
|
717
|
+
* intent; the sidecar holds the chosen mode per camera and publishes it in
|
|
718
|
+
* `CameraState.trackMode`, so the UI must reflect that server-confirmed value
|
|
719
|
+
* rather than this call (two browsers stay in agreement). While tracking, the
|
|
720
|
+
* camera also auto-zooms; `TrackMode.None` hands aiming/zoom back.
|
|
721
|
+
*/
|
|
722
|
+
async setTrackTarget(flightId, mode) {
|
|
723
|
+
await this._send({ type: "set-track-target", content: { flightId, mode } });
|
|
724
|
+
}
|
|
661
725
|
/**
|
|
662
726
|
* Internal — resolves the effective noise enabled state for a camera,
|
|
663
727
|
* merging the per-camera override (if any) with the client-level default.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export * from "./__generated__/types";
|
|
2
2
|
export { KerbcastClient, BrowserKerbcastTransport, type BrowserKerbcastTransportOptions, type InboundVideoStats, type KerbcastCameraHandle, type KerbcastClientConfig, type KerbcastConnectionState, type KerbcastClientEvents, type KerbcastCameraEvents, type KerbcastTransport, type KerbcastPeer, type KerbcastDataChannel, type DiscoveredCamera, type NoiseConfig, } from "./client";
|
|
3
3
|
export { PanZoomController, type PanZoomCommandSink, type PanZoomBounds, type PanZoomControllerOptions, } from "./panZoom";
|
|
4
|
+
export { RecordingController, createMediabunnyTrimmerLoader, negotiateMimeType, commonUtWindow, utToRecordingTimeMs, DEFAULT_COVERAGE_TIMEOUT_MS, ARM_TIMEOUT_MS, type ActiveRecordingInfo, type RecordingClient, type RecordingControllerOptions, type RecordingHandle, type RecordingsSnapshot, type GroupedRecordingHandle, type GroupedRecordingStart, type GroupTrimmer, type TrimmerLoader, type UtSample, type StartRecordingOptions, type StopGroupedRecordingOptions, } from "./recording";
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
export * from "./__generated__/types";
|
|
6
6
|
export { KerbcastClient, BrowserKerbcastTransport, } from "./client";
|
|
7
7
|
export { PanZoomController, } from "./panZoom";
|
|
8
|
+
export { RecordingController, createMediabunnyTrimmerLoader, negotiateMimeType, commonUtWindow, utToRecordingTimeMs, DEFAULT_COVERAGE_TIMEOUT_MS, ARM_TIMEOUT_MS, } from "./recording";
|