@ksp-gonogo/kerbcast 1.11.1 → 1.12.0-rc.2

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 (44) hide show
  1. package/README.md +229 -19
  2. package/dist/__generated__/types.d.ts +580 -14
  3. package/dist/__generated__/types.js +82 -1
  4. package/dist/anchors.d.ts +61 -0
  5. package/dist/anchors.js +71 -0
  6. package/dist/captureClip.d.ts +149 -0
  7. package/dist/captureClip.js +317 -0
  8. package/dist/client.d.ts +420 -27
  9. package/dist/client.js +1287 -58
  10. package/dist/commandTrack.d.ts +66 -6
  11. package/dist/commandTrack.js +137 -13
  12. package/dist/frameUt.d.ts +44 -10
  13. package/dist/frameUt.js +34 -4
  14. package/dist/index.d.ts +5 -3
  15. package/dist/index.js +3 -2
  16. package/dist/mp4.d.ts +125 -0
  17. package/dist/mp4.js +531 -0
  18. package/dist/panZoom.d.ts +1 -2
  19. package/dist/panZoom.js +1 -2
  20. package/dist/recording.d.ts +216 -16
  21. package/dist/recording.js +565 -50
  22. package/dist/testing/index.d.ts +41 -3
  23. package/dist/testing/index.js +97 -6
  24. package/package.json +1 -1
  25. package/src/__generated__/types.ts +579 -13
  26. package/src/anchors.test.ts +68 -0
  27. package/src/anchors.ts +110 -0
  28. package/src/captureClip.test.ts +631 -0
  29. package/src/captureClip.ts +379 -0
  30. package/src/client.test.ts +2681 -86
  31. package/src/client.ts +1499 -90
  32. package/src/commandTrack.test.ts +206 -10
  33. package/src/commandTrack.ts +160 -14
  34. package/src/framePipe.test.ts +79 -0
  35. package/src/frameUt.test.ts +15 -0
  36. package/src/frameUt.ts +72 -9
  37. package/src/index.ts +26 -0
  38. package/src/mp4.ts +678 -0
  39. package/src/panZoom.ts +1 -2
  40. package/src/recording.capture.test.ts +390 -0
  41. package/src/recording.test.ts +427 -11
  42. package/src/recording.ts +744 -66
  43. package/src/testing/index.ts +103 -8
  44. package/src/testing.test.ts +1 -1
package/README.md CHANGED
@@ -102,13 +102,59 @@ static and frame lock, so anything it does (holding frames back, dropping
102
102
  them) shows up in `cam.mediaStream` with the static still drawn on top.
103
103
 
104
104
  Kerbcast installs the transform on whichever receiver carries the camera. A
105
- transform instance works on one receiver only, so you pass a factory:
106
- Kerbcast calls it for a fresh instance when the camera is bound, again when a
107
- slot rebind moves the camera to another receiver, and again after a
108
- reconnect. A receiver the camera leaves goes back to passthrough, and frames
109
- the old instance still holds are dropped. `setEncodedTransform(null)`
105
+ transform instance works on one receiver only, so you pass a factory: Kerbcast
106
+ calls it for a fresh instance when the camera is bound, again when a slot
107
+ rebind moves the camera to another receiver, and again after a reconnect. It
108
+ also calls it again when the camera's stream starts over on the receiver it
109
+ kept: back in the list after a scene change (a relaunch, say), back from
110
+ destroyed, or reattached in place without leaving the list (a vessel switch
111
+ that rebuilds its camera; needs a sidecar that sends `attach`). The new frames
112
+ come from a new encoder on a new capture timeline, so an instance holding
113
+ state from the old one is replaced, in one step with no frames passing
114
+ untransformed. A receiver the camera leaves goes back to passthrough, and
115
+ frames the old instance still holds are dropped. `setEncodedTransform(null)`
110
116
  detaches. It works in both legacy and dynamic slot modes.
111
117
 
118
+ On the `"script"` path Chromium wires a script transform to its receiver
119
+ only as frames reach it, so on a receiver with no packets coming in (a
120
+ relaunch gap, a slot about to be fed) an instance built before it was handed
121
+ over can lose to the previous transform and receive nothing, for good.
122
+ The passthrough a receiver goes back to, or a transform given `holdMs` (see
123
+ below), is watched once packets arrive. One whose frames decode is left
124
+ alone. One that sees packets arrive with no frame decoded for 2 s past its
125
+ hold is renewed from the factory, which Chromium wires at once since packets
126
+ are flowing, and Kerbcast asks the sidecar for a keyframe after the swap. It
127
+ is renewed again, up to three more times, while packets keep arriving with no
128
+ frame decoded for 2 s, then 4 s, then 8 s past the hold. A transform with no
129
+ `holdMs` is renewed once when packets first arrive, and not checked after.
130
+ Build the transform inside the factory call
131
+ rather than ahead of it, and be ready for the factory to run again for the
132
+ same install. A factory that throws on a renewal leaves the previous instance in
133
+ place.
134
+
135
+ A transform that holds frames back, such as a delay, decodes nothing until
136
+ its hold has passed, so Kerbcast cannot tell it from one left without
137
+ frames. Give the hold, the longest a frame can sit inside the transform
138
+ including any pacing buffer, to have your transform checked the same way:
139
+
140
+ ```ts
141
+ cam.setEncodedTransform(factory, { holdMs: () => currentDelayMs() });
142
+ ```
143
+
144
+ `holdMs` is a number or a function, read at each check, so it can follow a
145
+ changing delay. With it, the transform is renewed only when packets keep
146
+ arriving undecoded for the hold plus 2 s (then plus 4 s, then plus 8 s, at
147
+ most three more times), so a delay that is working keeps the frames it
148
+ holds. The longest hold read (a function is read every 2 s while the
149
+ transform is installed) also tells Kerbcast how long a replaced instance may
150
+ go on decoding what it held, so the check of whatever replaces it on that
151
+ receiver, after a relaunch too, waits that out before it trusts decoded
152
+ frames or renews. Without it, every install is renewed once when
153
+ packets first arrive on its receiver, and that renewal drops whatever the
154
+ first instance was holding and waits for a keyframe. Whatever replaces it on
155
+ that receiver, the passthrough included, is also renewed once when packets
156
+ first arrive.
157
+
112
158
  It needs `encodedTransforms: true` when the client is constructed, and
113
159
  `setEncodedTransform` throws without it. The option gives every receiver a
114
160
  passthrough transform from the start, because Chrome only honours a receiver
@@ -199,7 +245,11 @@ and watches a craft from there, the way a tracking station would. The
199
245
  game's settings give each scene one, the Launchpad camera, and a client can
200
246
  create more at any vantage. Each appears in `client.cameras` with
201
247
  `kind: "command"` and a `commandCentre` describing where it stands and what
202
- it watches.
248
+ it watches. `commandCentre.siteKey` names the place the lens stands and
249
+ `siteKind` says what it is (`station`, `launchSite`, `surface` or `vessel`);
250
+ the key stays the same when the craft a `vessel` site rides docks, while
251
+ `vantagePersistentId` follows the craft it rides now. `stationId` is
252
+ deprecated.
203
253
 
204
254
  A created camera lasts for the rest of the game scene (a flight, or the
205
255
  Space Center while the game's command camera setting is on); leaving the
@@ -247,13 +297,26 @@ at the Space Center. `createCommandCamera` takes the same `aim` as an option.
247
297
  The aim is one of:
248
298
 
249
299
  - `vessel`: follows the craft with that `persistentId` (any entry of
250
- `commandCentre.targets`, whatever its eligibility)
300
+ `commandCentre.targets`, whatever its eligibility). Where a world model
301
+ governs the feed, a craft whose position it cannot narrow enough for the
302
+ view to frame is refused as `tooUncertain`; add `allowUncertain: true` to
303
+ aim at it anyway, as a deliberate search of the region it may be in. Under
304
+ the game's own knowledge every known craft is accepted, and while a command
305
+ lock is held browser aims are refused whatever they allow
251
306
  - `orbit`: follows a conic (`body`, `inclination`, `eccentricity`,
252
307
  `semiMajorAxis`, `lan`, `argumentOfPeriapsis` in degrees and metres,
253
308
  `meanAnomalyAtEpoch` in radians, `epoch`)
254
- - `positionAt`: follows a position you propagate yourself: `position` and
255
- `velocity` as `[x, y, z]` relative to `body` in its non-rotating frame at
256
- `ut`, carried on linearly
309
+ - `positionAt`: follows a craft coasting from a state you give: `position`
310
+ and `velocity` as `[x, y, z]` relative to `body` at `ut`, z up, in KSP's
311
+ fixed inertial frame, carried on ballistically under the body's gravity
312
+ with no thrust. From a KSP `Orbit` that frame is always
313
+ `Planetarium.Zup.LocalToWorld(orbit.getRelativePositionAtUT(ut))` (and the
314
+ same for `getOrbitalVelocityAtUT`, `orbit.pos`, `orbit.vel`); the raw
315
+ values are not, since KSP leaves Zup turned after a low flight. A browser
316
+ client has no `Orbit`, so it takes these vectors from whatever mod or model
317
+ hands it craft states in that frame. A velocity of zero no longer holds a
318
+ point still (it falls); use `surface` for a fixed point. Before 1.12 this
319
+ was Zup's own frame, carried on linearly
257
320
  - `surface`: holds a point on a `body` (`latitude`, `longitude`, `altitude`
258
321
  in metres above sea level), turning with the planet
259
322
  - `direction`: holds a direction from the camera's local horizon,
@@ -377,9 +440,9 @@ before or during the lock are not restored.
377
440
 
378
441
  The lock holds until its holder releases it (or the scene changes); it never
379
442
  times out. A holder that stops calling for 5 s blanks every command feed
380
- ("gonogo not responding") but keeps the lock, so aims from anyone else are
381
- still refused. This is deliberate: a holder that delays commands (gonogo
382
- applies signal delay) can not be bypassed while it is unresponsive.
443
+ ("Authority not responding") but keeps the lock, so aims from anyone else are
444
+ still refused. This is deliberate: a holder that delays commands (one that
445
+ applies signal delay, say) can not be bypassed while it is unresponsive.
383
446
 
384
447
  ### In game (C#)
385
448
 
@@ -406,15 +469,15 @@ Holding the lock:
406
469
 
407
470
  ```csharp
408
471
  // Re-take it on every scene load; a different owner is refused.
409
- KerbcastControl.SetCommandAuthority("gonogo");
472
+ KerbcastControl.SetCommandAuthority("mission-control");
410
473
 
411
474
  // Only calls passing the owner are accepted now, and each is a heartbeat.
412
475
  KerbcastControl.SetCommandAim(pad, CommandAim.PositionAt("Kerbin", ut, pos, vel,
413
- subject: craft.persistentId, framingMarginM: uncertaintyM), "gonogo");
476
+ subject: craft.persistentId, framingMarginM: uncertaintyM), "mission-control");
414
477
  KerbcastControl.SetCommandAim(pad, CommandAim.Vessel(other)); // false: locked
415
478
 
416
479
  // Every command camera returns to its default.
417
- KerbcastControl.ClearCommandAuthority("gonogo");
480
+ KerbcastControl.ClearCommandAuthority("mission-control");
418
481
  ```
419
482
 
420
483
  - `CommandVantage.LaunchSite(site, distance, bearing, height)`,
@@ -438,16 +501,163 @@ KerbcastControl.ClearCommandAuthority("gonogo");
438
501
  - The lock holds until `ClearCommandAuthority` or a scene change, and fails
439
502
  closed: a holder that makes no call (`SetCommandAuthority`, `SetCommandAim`
440
503
  with its owner, `SetCommandBlank`) for 5 s of real time blanks every
441
- command feed "gonogo not responding" and keeps the lock, so everyone else
504
+ command feed "Authority not responding" and keeps the lock, so everyone else
442
505
  stays refused. `SetCommandBlank(flightId, reason)` blanks one feed
443
506
  - `DestroyCommandCamera(flightId)` returns false for the settings camera or
444
507
  an unknown id
445
- - `CommandFeeds()` lists every built command camera, with its `Aim`,
446
- `SubjectPersistentId`, and `Created` set on the ones made at runtime;
508
+ - `CommandFeeds()` lists every built command camera, with its `SiteKey`,
509
+ `SiteKind`, `VantagePersistentId`, `Aim`, `SubjectPersistentId`, and
510
+ `Created` set on the ones made at runtime (`StationId` is obsolete);
447
511
  `CommandTargets(flightId)` lists the craft it could follow (a craft docked
448
512
  into another drops out; the combined craft keeps the survivor's
449
513
  `persistentId`)
450
514
 
515
+ ## Anchored markers
516
+
517
+ Anchors mark things in the game on a camera's picture. The game projects
518
+ each one through the camera that rendered every frame (its pose and field
519
+ of view at that moment, so zoom and pan are exact), and the result rides
520
+ that frame's stamp. Markers drawn from it line up with the picture under
521
+ any stream delay. There is no live position: everything is tied to frames.
522
+
523
+ ```ts
524
+ import { AnchorDirection, AnchorKind } from "@ksp-gonogo/kerbcast";
525
+
526
+ await client.setAnchors(flightId, [
527
+ { id: "port", kind: AnchorKind.Part, vessel: targetPid, part: portPid, orientation: true },
528
+ { id: "ship", kind: AnchorKind.Vessel, vessel: targetPid, occlusion: true },
529
+ { id: "site", kind: AnchorKind.Surface, body: "Mun", latitude: 2.4, longitude: 81.5 },
530
+ { id: "pro", kind: AnchorKind.Direction, direction: AnchorDirection.TargetRelativeVelocity },
531
+ ]);
532
+ await client.setAnchors(flightId, null); // clear
533
+ ```
534
+
535
+ - Kinds: `part` (craft and part persistentId, optional `offset` in the
536
+ part's axes; a docking port is marked at its docking face; follows the
537
+ part through docking), `vessel` (centre of mass; follows a craft merged
538
+ into another), `surface` (body, latitude, longitude, altitude) and
539
+ `direction` (a named direction of a craft, the active one by default, or
540
+ a fixed inertial `vector`), drawn at infinity, and `kinematic` (a point
541
+ you describe with `kinematics`: a `conic` from KSP Orbit elements, a
542
+ `stateVector` coasting ballistically in the same fixed frame as a
543
+ `positionAt` aim, or a `surface` point, each about a named `body`), for a
544
+ last-known ghost, a hypothesis or a predicted point
545
+ - `orientation: true` on a part or vessel adds its `forward` and `up` axes
546
+ in camera space (x right, y up, z forward) to every result.
547
+ `occlusion: true` adds `occluded`, at the cost of a ray cast per frame.
548
+ `marginM` pads the radius `apparentRadiusPx` is worked out from
549
+ - Anchors belong to the client that set them: other clients never see them,
550
+ the command lock does not apply, and the sidecar drops them when the
551
+ client disconnects. The client keeps them until cleared and sends them
552
+ again on reconnect; anchors on a camera the sidecar does not list wait
553
+ until it appears. At most `client.maxAnchorsPerCamera` per camera, which
554
+ the sidecar announces in `hello` (16 when it does not say,
555
+ `MAX_ANCHORS_PER_CAMERA`)
556
+ - Needs a 1.12 sidecar and plugin; against an older sidecar they are kept
557
+ but never sent (`client.anchorsSupported`)
558
+
559
+ Each result lands on the displayed frame through `watchDisplayedFrames`:
560
+
561
+ ```ts
562
+ import { anchorPoint, fitFrame, frameToScreen, frameLengthToScreen, findAnchor } from "@ksp-gonogo/kerbcast";
563
+
564
+ client.watchDisplayedFrames(flightId, video, (frame) => {
565
+ const port = findAnchor(frame.anchors, "port");
566
+ const point = port && anchorPoint(port); // null when the result carries no position
567
+ if (!point || !frame.frameWidth || !frame.frameHeight) return;
568
+ const fitted = fitFrame(video.clientWidth, video.clientHeight,
569
+ frame.frameWidth, frame.frameHeight, "contain"); // or "cover"
570
+ const { x, y } = frameToScreen(point, frame.frameWidth, frame.frameHeight, fitted);
571
+ const r = frameLengthToScreen(port.apparentRadiusPx ?? 0, frame.frameHeight, fitted);
572
+ draw(x, y, r, port.inFrame, port.behind);
573
+ });
574
+ ```
575
+
576
+ Each result has `x`, `y` (capture pixels from the top left), `inFrame`,
577
+ `behind` (then `x`, `y` sit off the frame on the side it is nearest),
578
+ `distanceM` (absent for directions), `apparentRadiusPx`, `angleOffAxis`
579
+ (degrees), and `forward`, `up`, `occluded` when asked for. Anchors set in
580
+ game carry their `owner`; this client's own have none. Every stamp also
581
+ gives the frame's `fov` and size, for overlays that do their own maths.
582
+
583
+ An `observed: true` result carries only what the picture shows: `x`, `y`,
584
+ `apparentRadiusPx` (and `forward`, `up`) only while the anchor is in frame and
585
+ not occluded, and never `distanceM`, `angleOffAxis` or `behind`. Out of view it
586
+ is just `inFrame` and `occluded`. Use `anchorPoint(result)`, which is `null`
587
+ when there is no position.
588
+
589
+ A command camera adds its own anchor: `id: "subject"`, owner `"kerbcast"`
590
+ (`COMMAND_SUBJECT_ANCHOR_ID`, `KERBCAST_ANCHOR_OWNER`), a `vessel` anchor on
591
+ the craft it watches with occlusion on. It is always observed, so it never
592
+ gives away a craft the picture does not show. The React brackets follow it.
593
+ A second, `id: "aim"` (`COMMAND_AIM_ANCHOR_ID`), is where the aim point lands
594
+ in the frame: the centre, except while the camera turns onto a moved aim.
595
+
596
+ While a command authority holds the lock, every browser client's anchors
597
+ are observed-only by default. The holder can relax that
598
+ (`KerbcastControl.SetAnchorPolicy(AnchorPolicy.Full, owner)`, for every
599
+ camera or one) and can withhold the subject anchor
600
+ (`KerbcastControl.SetSubjectAnchor(false, owner)`). Both return to the
601
+ unrestricted behaviour when the lock clears. A `kinematic` anchor is never
602
+ observed: it marks what you supplied, not what the game shows. Where it would
603
+ be observed, though, its result still withholds the lens's pose: `x`, `y` and
604
+ `apparentRadiusPx` only while `inFrame`, and never `distanceM`, `behind` or
605
+ `angleOffAxis`. Under the lock
606
+ `commandTrack.trueOffsetPx` is likewise sent only while the true craft is in
607
+ frame and not occluded.
608
+
609
+ ### State that rides the frame
610
+
611
+ From 1.12 every frame's stamp carries what that frame shows, so a view that
612
+ delays the picture never learns it sooner.
613
+
614
+ A light speed comes only from the game: the mod holding the command lock
615
+ sets it through Kerbcast.API. Without one the picture is live, so a command
616
+ camera's `subjectUt` equals the frame's `ut`.
617
+
618
+ - `liveLightTimeS` on every frame of every camera: the light time covering
619
+ what of the lens's own craft and site the frame draws at its `ut`. It is
620
+ 0 with no light speed. A delayed view releases the frame no sooner than
621
+ its `ut` plus this plus its own delay from the lens. Absent means unknown
622
+ (the game could not read it, or it is older than 1.12): under a light
623
+ speed, hold the frame. Other loaded craft near the lens are still drawn at
624
+ `ut` and are not covered yet; once craft beyond the boundary are hidden,
625
+ the bound covers everything drawn at `ut`
626
+ - `boundaryM`: how far light travels in one 60 fps frame at the light
627
+ speed, the picture's boundary distance. Absent with no light speed
628
+ - `command` on a command camera's frames: its fix, range, bracket,
629
+ occlusion, sunlight, blank reason, lock owner, subject, the subject's
630
+ refusal (`unknown`, `notPositioned`, `refused`, `stale`) and which world
631
+ model snapshot it read
632
+
633
+ All three arrive on `DisplayedFrameUt` through `watchDisplayedFrames`.
634
+ `effectiveCommandTrack(track, displayUt, frame.command)` uses a frame's own
635
+ state whole; the React feed does this for its brackets and readout.
636
+
637
+ A host with its own delayed playout keeps the `DisplayedFrameUt` it got for
638
+ each frame and hands the one it is showing to the React feed as
639
+ `displayFrame` (beside, and winning over, `displayUt`): its UT picks the
640
+ transition in force and its `command` draws the brackets and readout. A UT
641
+ alone carries no frame state, so under a lock `displayUt` by itself shows no
642
+ fix. Without either, the feed uses the frame the browser presents, or the
643
+ newest stamped frame before it, which lags the picture and so is never early.
644
+
645
+ While a command lock is held, the camera list's `commandTrack` keeps only its
646
+ `ut`, `authority` and `events` (each replayed at its own UT); the other fields
647
+ read as their defaults, which a 1.11 client shows as no fix. The targets list
648
+ in `commandCentre` carries the `ut` it describes:
649
+ `client.commandCentreAt(flightId, displayUt)` gives the list as it stood at
650
+ the displayed frame, and `null` while every list sent is newer. With no
651
+ displayed UT it gives the newest, except under a lock, where it gives `null`.
652
+ Lists are kept back to two hours of UT before the newest, at most 600 per
653
+ camera (a UT older than every list kept finds none), and are forgotten when
654
+ the camera leaves the list or the client reconnects.
655
+
656
+ In game, `KerbcastControl.SetAnchors(flightId, anchors, owner)` does the same
657
+ with `AnchorSpec.ForPart`, `ForVessel`, `ForSurface`, `ForDirection` and
658
+ `ForVector`. Those results go to every client that asked for anchors, tagged
659
+ with `owner`, until replaced, `ClearAnchors(owner)` or a scene change.
660
+
451
661
  ## Pan/zoom input helper
452
662
 
453
663
  Driving pan and zoom from a gamepad stick, nudge buttons, or an FoV