@ksp-gonogo/kerbcast 1.6.2 → 1.6.3
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/dist/__generated__/types.d.ts +14 -11
- package/dist/client.js +33 -0
- package/package.json +1 -1
- package/src/__generated__/types.ts +14 -11
- package/src/client.ts +36 -0
|
@@ -131,8 +131,8 @@ export interface CameraState {
|
|
|
131
131
|
*/
|
|
132
132
|
targetBitrateBps: number;
|
|
133
133
|
/**
|
|
134
|
-
* Effective degrade level (
|
|
135
|
-
* requests). 0.0 = perfect, 1.0 = max degradation. Applied
|
|
134
|
+
* Effective degrade level (last-writer-wins across subscribers'
|
|
135
|
+
* SetDegrade requests). 0.0 = perfect, 1.0 = max degradation. Applied
|
|
136
136
|
* alongside operator render-size + adaptive shed; the encoder
|
|
137
137
|
* multiplies its effective bitrate by `(1 - 0.7 * level)` and
|
|
138
138
|
* skips fan-out for a fraction of frames at high levels.
|
|
@@ -187,10 +187,12 @@ export interface SceneStateChangedPayload {
|
|
|
187
187
|
export interface SetDegradePayload {
|
|
188
188
|
flightId: number;
|
|
189
189
|
/**
|
|
190
|
-
* 0.0 = perfect quality, 1.0 = maximum degradation.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
190
|
+
* 0.0 = perfect quality, 1.0 = maximum degradation. Clamped.
|
|
191
|
+
* Last-writer-wins: degrade is a property of the camera's game
|
|
192
|
+
* state (comms strength), shared by all viewers of the single
|
|
193
|
+
* encoded stream, so the sidecar stores the latest value rather
|
|
194
|
+
* than reducing across subscribers. A well-behaved driver
|
|
195
|
+
* re-asserts every tick.
|
|
194
196
|
*/
|
|
195
197
|
level: number;
|
|
196
198
|
}
|
|
@@ -379,11 +381,12 @@ export type ClientMessage =
|
|
|
379
381
|
}
|
|
380
382
|
/**
|
|
381
383
|
* Request artificial signal degradation. 0.0 = perfect quality,
|
|
382
|
-
* 1.0 = maximum degradation.
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* to the sidecar "I want to look
|
|
386
|
-
* antenna is struggling"; sidecar takes
|
|
384
|
+
* 1.0 = maximum degradation. Last-writer-wins: the sidecar stores
|
|
385
|
+
* the latest value on the camera (degrade is shared across all
|
|
386
|
+
* viewers of the one encoded stream), so a departed peer can't
|
|
387
|
+
* pin it. Lets consumers signal to the sidecar "I want to look
|
|
388
|
+
* like the in-game CommNet antenna is struggling"; sidecar takes
|
|
389
|
+
* the opportunity to
|
|
387
390
|
* reduce bitrate + skip frames, which both creates the
|
|
388
391
|
* signal-loss aesthetic AND saves encoder CPU.
|
|
389
392
|
*/
|
package/dist/client.js
CHANGED
|
@@ -4,6 +4,13 @@ import { tryCreateNoisePipeline } from "./noise";
|
|
|
4
4
|
const SOURCELESS_INTENSITY = 1.0;
|
|
5
5
|
/** Floor applied to the degrade-driven intensity of a live feed. */
|
|
6
6
|
const LIVE_INTENSITY_FLOOR = 0.05;
|
|
7
|
+
/**
|
|
8
|
+
* While a feed is stalled, how often to re-request a keyframe. A stall is
|
|
9
|
+
* usually a wedged decoder that only a fresh IDR clears; the first request or
|
|
10
|
+
* its keyframe can be lost during the same outage, so we keep asking until
|
|
11
|
+
* frames resume.
|
|
12
|
+
*/
|
|
13
|
+
const STALL_KEYFRAME_RETRY_MS = 2000;
|
|
7
14
|
export class BrowserKerbcastTransport {
|
|
8
15
|
constructor(opts = {}) {
|
|
9
16
|
this.opts = { iceGatheringTimeoutMs: opts.iceGatheringTimeoutMs ?? 2000 };
|
|
@@ -159,6 +166,8 @@ class CameraHandle extends TypedEmitter {
|
|
|
159
166
|
this._sourceless = false;
|
|
160
167
|
/** Stall state mirrored from the pipeline's onStallChange callback. */
|
|
161
168
|
this._stalled = false;
|
|
169
|
+
/** Interval that re-requests keyframes while stalled; null when not stalled. */
|
|
170
|
+
this._keyframeRetry = null;
|
|
162
171
|
/** Whether animated static is drawn; survives pipeline rebuilds. */
|
|
163
172
|
this._showStatic = true;
|
|
164
173
|
this.flightId = flightId;
|
|
@@ -291,6 +300,26 @@ class CameraHandle extends TypedEmitter {
|
|
|
291
300
|
return;
|
|
292
301
|
this._stalled = stalled;
|
|
293
302
|
this.emit("stall", stalled);
|
|
303
|
+
if (stalled) {
|
|
304
|
+
// A stall usually means the browser decoder is wedged (broken H.264
|
|
305
|
+
// reference chain after packet loss or heavy degrade), which only a
|
|
306
|
+
// fresh keyframe clears. Force one now and keep asking while stalled,
|
|
307
|
+
// rather than waiting on the browser's rate-limited PLI.
|
|
308
|
+
this._requestKeyframeForRecovery();
|
|
309
|
+
if (this._keyframeRetry === null) {
|
|
310
|
+
this._keyframeRetry = setInterval(() => this._requestKeyframeForRecovery(), STALL_KEYFRAME_RETRY_MS);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (this._keyframeRetry !== null) {
|
|
314
|
+
clearInterval(this._keyframeRetry);
|
|
315
|
+
this._keyframeRetry = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
_requestKeyframeForRecovery() {
|
|
319
|
+
this.requestKeyframe().catch(() => {
|
|
320
|
+
// Best effort; a failed send (disconnected) is retried on the next tick,
|
|
321
|
+
// or cleared when the stall ends.
|
|
322
|
+
});
|
|
294
323
|
}
|
|
295
324
|
_liveIntensity() {
|
|
296
325
|
return Math.max(LIVE_INTENSITY_FLOOR, this._state?.degradeLevel ?? 0);
|
|
@@ -300,6 +329,10 @@ class CameraHandle extends TypedEmitter {
|
|
|
300
329
|
* the persistent pipeline, stops its rAF loop, and nulls the stream.
|
|
301
330
|
*/
|
|
302
331
|
_teardown() {
|
|
332
|
+
if (this._keyframeRetry !== null) {
|
|
333
|
+
clearInterval(this._keyframeRetry);
|
|
334
|
+
this._keyframeRetry = null;
|
|
335
|
+
}
|
|
303
336
|
this._noisePipeline?.destroy();
|
|
304
337
|
this._noisePipeline = null;
|
|
305
338
|
this._rawStream = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksp-gonogo/kerbcast",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "TypeScript client for the kerbcast KSP camera-streaming sidecar. KerbcastClient wraps the WebRTC peer + control channel; wire-format types are typeshare-generated from the sidecar's Rust protocol module.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jon Pepler",
|
|
@@ -139,8 +139,8 @@ export interface CameraState {
|
|
|
139
139
|
*/
|
|
140
140
|
targetBitrateBps: number;
|
|
141
141
|
/**
|
|
142
|
-
* Effective degrade level (
|
|
143
|
-
* requests). 0.0 = perfect, 1.0 = max degradation. Applied
|
|
142
|
+
* Effective degrade level (last-writer-wins across subscribers'
|
|
143
|
+
* SetDegrade requests). 0.0 = perfect, 1.0 = max degradation. Applied
|
|
144
144
|
* alongside operator render-size + adaptive shed; the encoder
|
|
145
145
|
* multiplies its effective bitrate by `(1 - 0.7 * level)` and
|
|
146
146
|
* skips fan-out for a fraction of frames at high levels.
|
|
@@ -203,10 +203,12 @@ export interface SceneStateChangedPayload {
|
|
|
203
203
|
export interface SetDegradePayload {
|
|
204
204
|
flightId: number;
|
|
205
205
|
/**
|
|
206
|
-
* 0.0 = perfect quality, 1.0 = maximum degradation.
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
206
|
+
* 0.0 = perfect quality, 1.0 = maximum degradation. Clamped.
|
|
207
|
+
* Last-writer-wins: degrade is a property of the camera's game
|
|
208
|
+
* state (comms strength), shared by all viewers of the single
|
|
209
|
+
* encoded stream, so the sidecar stores the latest value rather
|
|
210
|
+
* than reducing across subscribers. A well-behaved driver
|
|
211
|
+
* re-asserts every tick.
|
|
210
212
|
*/
|
|
211
213
|
level: number;
|
|
212
214
|
}
|
|
@@ -385,11 +387,12 @@ export type ClientMessage =
|
|
|
385
387
|
| { type: "set-zoom-rate", content: SetZoomRatePayload }
|
|
386
388
|
/**
|
|
387
389
|
* Request artificial signal degradation. 0.0 = perfect quality,
|
|
388
|
-
* 1.0 = maximum degradation.
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* to the sidecar "I want to look
|
|
392
|
-
* antenna is struggling"; sidecar takes
|
|
390
|
+
* 1.0 = maximum degradation. Last-writer-wins: the sidecar stores
|
|
391
|
+
* the latest value on the camera (degrade is shared across all
|
|
392
|
+
* viewers of the one encoded stream), so a departed peer can't
|
|
393
|
+
* pin it. Lets consumers signal to the sidecar "I want to look
|
|
394
|
+
* like the in-game CommNet antenna is struggling"; sidecar takes
|
|
395
|
+
* the opportunity to
|
|
393
396
|
* reduce bitrate + skip frames, which both creates the
|
|
394
397
|
* signal-loss aesthetic AND saves encoder CPU.
|
|
395
398
|
*/
|
package/src/client.ts
CHANGED
|
@@ -15,6 +15,13 @@ import { type NoisePipeline, tryCreateNoisePipeline } from "./noise";
|
|
|
15
15
|
const SOURCELESS_INTENSITY = 1.0;
|
|
16
16
|
/** Floor applied to the degrade-driven intensity of a live feed. */
|
|
17
17
|
const LIVE_INTENSITY_FLOOR = 0.05;
|
|
18
|
+
/**
|
|
19
|
+
* While a feed is stalled, how often to re-request a keyframe. A stall is
|
|
20
|
+
* usually a wedged decoder that only a fresh IDR clears; the first request or
|
|
21
|
+
* its keyframe can be lost during the same outage, so we keep asking until
|
|
22
|
+
* frames resume.
|
|
23
|
+
*/
|
|
24
|
+
const STALL_KEYFRAME_RETRY_MS = 2000;
|
|
18
25
|
|
|
19
26
|
/** Controls the digital-static noise overlay baked into `cam.mediaStream`. */
|
|
20
27
|
export interface NoiseConfig {
|
|
@@ -473,6 +480,8 @@ class CameraHandle
|
|
|
473
480
|
private _sourceless = false;
|
|
474
481
|
/** Stall state mirrored from the pipeline's onStallChange callback. */
|
|
475
482
|
private _stalled = false;
|
|
483
|
+
/** Interval that re-requests keyframes while stalled; null when not stalled. */
|
|
484
|
+
private _keyframeRetry: ReturnType<typeof setInterval> | null = null;
|
|
476
485
|
/** Whether animated static is drawn; survives pipeline rebuilds. */
|
|
477
486
|
private _showStatic = true;
|
|
478
487
|
private readonly client: KerbcastClient;
|
|
@@ -625,6 +634,29 @@ class CameraHandle
|
|
|
625
634
|
if (this._stalled === stalled) return;
|
|
626
635
|
this._stalled = stalled;
|
|
627
636
|
this.emit("stall", stalled);
|
|
637
|
+
if (stalled) {
|
|
638
|
+
// A stall usually means the browser decoder is wedged (broken H.264
|
|
639
|
+
// reference chain after packet loss or heavy degrade), which only a
|
|
640
|
+
// fresh keyframe clears. Force one now and keep asking while stalled,
|
|
641
|
+
// rather than waiting on the browser's rate-limited PLI.
|
|
642
|
+
this._requestKeyframeForRecovery();
|
|
643
|
+
if (this._keyframeRetry === null) {
|
|
644
|
+
this._keyframeRetry = setInterval(
|
|
645
|
+
() => this._requestKeyframeForRecovery(),
|
|
646
|
+
STALL_KEYFRAME_RETRY_MS,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
} else if (this._keyframeRetry !== null) {
|
|
650
|
+
clearInterval(this._keyframeRetry);
|
|
651
|
+
this._keyframeRetry = null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private _requestKeyframeForRecovery(): void {
|
|
656
|
+
this.requestKeyframe().catch(() => {
|
|
657
|
+
// Best effort; a failed send (disconnected) is retried on the next tick,
|
|
658
|
+
// or cleared when the stall ends.
|
|
659
|
+
});
|
|
628
660
|
}
|
|
629
661
|
|
|
630
662
|
private _liveIntensity(): number {
|
|
@@ -636,6 +668,10 @@ class CameraHandle
|
|
|
636
668
|
* the persistent pipeline, stops its rAF loop, and nulls the stream.
|
|
637
669
|
*/
|
|
638
670
|
_teardown(): void {
|
|
671
|
+
if (this._keyframeRetry !== null) {
|
|
672
|
+
clearInterval(this._keyframeRetry);
|
|
673
|
+
this._keyframeRetry = null;
|
|
674
|
+
}
|
|
639
675
|
this._noisePipeline?.destroy();
|
|
640
676
|
this._noisePipeline = null;
|
|
641
677
|
this._rawStream = null;
|