@camstack/types 1.2.245 → 1.2.247

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Source ids of a device COLLECTION that exactly one addon may mint (D625).
3
+ *
4
+ * A collection cap's picker treats its rows as peers, and that is right almost
5
+ * everywhere. It is wrong for exactly one thing: a DEFAULT that must be
6
+ * impossible to express otherwise.
7
+ *
8
+ * `recording`'s default is ours, always (the operator's ruling, 2026-09-24).
9
+ * The way that is made structural rather than a sort order is a RESERVED id:
10
+ * the absence of an operator choice MEANS `camstack`, so a surface never needs
11
+ * a "first row" fallback and there is nowhere to put one. `clip-sources.ts`
12
+ * ends with `?? sources[0]`, and `videoclips.listSources` on 592 answers the
13
+ * ONBOARD row first — for clips that fallback was argued for, for a timeline it
14
+ * would silently swap an 85 362 s bar for a 21 388 s one.
15
+ *
16
+ * A reserved id is only worth anything if nothing else can spell it, so it is
17
+ * enforced twice and neither is a paragraph:
18
+ *
19
+ * 1. `device-collection-dispatch.ts` DROPS a `listSources` row carrying a
20
+ * reserved id from any addon but its owner, naming the impostor in a log
21
+ * line with `tags: { deviceId }`. It drops the row rather than throwing:
22
+ * one lying provider must not empty a camera's picker.
23
+ * 2. `scripts/check-reserved-collection-sources.ts` fails CI on any package
24
+ * but the owner's that spells the id at all. It reads THIS table, so a new
25
+ * reservation is guarded the moment it is added here.
26
+ */
27
+ /** One reservation: `sourceId` on `capName` may be served only by `addonId`. */
28
+ export interface ReservedCollectionSource {
29
+ readonly capName: string;
30
+ readonly sourceId: string;
31
+ readonly addonId: string;
32
+ }
33
+ /**
34
+ * Every reserved (cap, source) pair in the system.
35
+ *
36
+ * KEEP THE LITERALS INLINE. The CI guard parses this array textually — it
37
+ * cannot execute TypeScript — so an entry built from an imported constant
38
+ * would be invisible to it, and a guard that reads the wrong surface is worse
39
+ * than none.
40
+ */
41
+ export declare const RESERVED_COLLECTION_SOURCES: readonly ReservedCollectionSource[];
42
+ /**
43
+ * The addon allowed to serve `sourceId` on `capName`, or `null` when the id is
44
+ * not reserved at all (which is the common case — most sources are peers).
45
+ */
46
+ export declare function reservedSourceOwner(capName: string, sourceId: string): string | null;
@@ -67,6 +67,7 @@ export declare const ClipStillRefusalCodeSchema: z.ZodEnum<{
67
67
  "no-keyframe": "no-keyframe";
68
68
  "queue-full": "queue-full";
69
69
  "camera-backoff": "camera-backoff";
70
+ "decode-failed": "decode-failed";
70
71
  "no-catalog-row": "no-catalog-row";
71
72
  "unknown-device": "unknown-device";
72
73
  "uid-missing": "uid-missing";
@@ -249,11 +250,36 @@ export type ClipWake = z.infer<typeof ClipWakeSchema>;
249
250
  * the size in the message, never truncates. Half a video is worse than an
250
251
  * honest refusal.
251
252
  *
252
- * The clean follow-on is a CHUNKED read so a `high` twin of a long clip stops
253
- * being refusable at all. That is a later slice, named here so the bound is
254
- * not mistaken for a design ceiling.
253
+ * **Superseded as the way a clip export gets its bytes** (D613). The bound is
254
+ * a property of the ENVELOPE, not of clips, so the cure was a transport with
255
+ * no envelope: {@link videoclipsCapability.methods.offerClipBytes} hands back
256
+ * a one-shot `peerBytes` ticket and the consumer streams it straight to disk,
257
+ * holding nothing. This method stays for a consumer that genuinely wants the
258
+ * bytes in hand, and for a provider not yet redeployed — with this bound,
259
+ * which for an inline read is not going to move.
255
260
  */
256
261
  export declare const VIDEOCLIPS_MAX_READ_BYTES: number;
262
+ /**
263
+ * Hard ceiling on ONE {@link videoclipsCapability.methods.offerClipBytes}
264
+ * transfer — ten times {@link VIDEOCLIPS_MAX_READ_BYTES}, and the reasoning is
265
+ * not "ten times more comfortable".
266
+ *
267
+ * The inline bound is set by what is HELD: a base64 envelope is the payload at
268
+ * ~1.33× in the provider AND the same again in the caller, so 50 MiB of clip
269
+ * is ~133 MiB of heap across two processes. Over `peerBytes` the consumer
270
+ * holds one socket chunk at a time and writes straight through to the export
271
+ * file, so its contribution is flat whatever the clip weighs. What is left is
272
+ * the PRODUCER's own copy, and that is a property of how each provider makes a
273
+ * clip rather than of this transport: Hikvision's fetch writes to a scratch
274
+ * FILE and offers it (nothing is held), Reolink's cmd-5 transfer is collected
275
+ * in memory and offered from there (one copy, the one it already had).
276
+ *
277
+ * So this number bounds the worst provider, not the transport, and it is named
278
+ * separately so that lifting it is a statement about a provider rather than
279
+ * about clips. Above it the provider REFUSES with the size in the message and
280
+ * never truncates: half a video is worse than an honest refusal.
281
+ */
282
+ export declare const VIDEOCLIPS_MAX_OFFER_BYTES: number;
257
283
  /**
258
284
  * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
259
285
  *
@@ -273,6 +299,34 @@ export declare const ClipBytesSchema: z.ZodObject<{
273
299
  durationMs: z.ZodOptional<z.ZodNumber>;
274
300
  }, z.core.$strip>;
275
301
  export type ClipBytes = z.infer<typeof ClipBytesSchema>;
302
+ /**
303
+ * Where a clip's finished bytes can be TAKEN (D613) — the answer to
304
+ * {@link videoclipsCapability.methods.offerClipBytes}.
305
+ *
306
+ * Everything {@link ClipBytesSchema} carries except the bytes themselves, plus
307
+ * the one-shot ticket that leads to them. The metadata is answered BEFORE the
308
+ * transfer on purpose: a consumer learns which twin it got, what to call the
309
+ * file and how long the clip runs without having to read a byte, so a decision
310
+ * it would make on that metadata (a wrong twin, an implausible duration) costs
311
+ * no transfer at all.
312
+ */
313
+ export declare const ClipBytesOfferSchema: z.ZodObject<{
314
+ ticket: z.ZodObject<{
315
+ url: z.ZodString;
316
+ hostNodeId: z.ZodString;
317
+ expiresAtMs: z.ZodNumber;
318
+ declaredBytes: z.ZodNullable<z.ZodNumber>;
319
+ }, z.core.$strip>;
320
+ contentType: z.ZodString;
321
+ name: z.ZodString;
322
+ served: z.ZodEnum<{
323
+ high: "high";
324
+ mid: "mid";
325
+ low: "low";
326
+ }>;
327
+ durationMs: z.ZodOptional<z.ZodNumber>;
328
+ }, z.core.$strip>;
329
+ export type ClipBytesOffer = z.infer<typeof ClipBytesOfferSchema>;
276
330
  /**
277
331
  * Where a clip's STREAM can be dialled (D597) — the answer to
278
332
  * {@link videoclipsCapability.methods.dialClipStream}.
@@ -338,6 +392,125 @@ export declare const ClipStreamDialSchema: z.ZodObject<{
338
392
  }>>;
339
393
  }, z.core.$strip>;
340
394
  export type ClipStreamDial = z.infer<typeof ClipStreamDialSchema>;
395
+ /**
396
+ * The playback rates the broker can pace over media it HOLDS, ascending,
397
+ * always with `1` — and the WIDEST ladder there is. It is the domain the
398
+ * broker's `clampPlaybackRate` is derived from, at both ends.
399
+ *
400
+ * These are the BROKER's: it re-paces frames it has already demuxed — the
401
+ * `MonotonicClock` divides source elapsed time by the factor and the pacer
402
+ * pushes that much faster. `1.5` is in it because a re-pacer has no reason to
403
+ * refuse it.
404
+ *
405
+ * **`8` is here since D621, and it is the pacer's, not a camera's.** D620
406
+ * measured the thing that was assumed for a year and was never true: a
407
+ * Reolink `<playSpeed>` does not time-compress anything. At 1, 2, 4 and 8 the
408
+ * transfer is byte-identical — same 947 472 bytes, same 594 access units,
409
+ * same 39.855 s presentation span, same 624 AAC frames — and only the wall
410
+ * clock moves. It buys SUPPLY, never speed. So there was never a second
411
+ * mechanism to wire for 8×: there is one, this one, and what it needs is
412
+ * media in hand and a clamp that does not move the number.
413
+ *
414
+ * **`16` is deliberately NOT here, and will not join by widening this array.**
415
+ * The camera's `playSpeed 16` is not a rate at all but an I-frame-only MODE —
416
+ * measured on 592: 10 access units for a whole 36.3 s sub clip, 20 for the
417
+ * main twin at a 2 025 ms median spacing, and no audio track. That is
418
+ * different CONTENT, a stream of its own, and the honest broker-side twin of
419
+ * it would be a pacer that DROPS whole GOPs rather than one that pushes 16×
420
+ * the bitrate at a live WebRTC track. Neither exists. A `16` in this array
421
+ * today would be a rate accepted and delivered at 8 — D612's defect, which
422
+ * this list exists to end, one number further along.
423
+ *
424
+ * Every entry must survive the broker's `clampPlaybackRate` unchanged. Since
425
+ * D621 that is structural rather than a discipline: the clamp reads this
426
+ * array's own ends. What still has to be proved behaviourally — and is, in
427
+ * the broker's `clip-stream-feeder.spec` — is that the PACER really empties a
428
+ * clip `rate` times faster, because a clamp agreeing with a ladder is a
429
+ * constant compared against itself.
430
+ *
431
+ * Narrower ladders exist for transports whose SUPPLY is bounded; they are
432
+ * subsets of this one ({@link CLIP_STREAM_SOURCE_RATES},
433
+ * {@link CLIP_REALTIME_SOURCE_RATES}).
434
+ */
435
+ export declare const CLIP_BROKER_PACED_RATES: readonly number[];
436
+ /**
437
+ * The rates a provider-STREAMED clip can be delivered at — a subset of
438
+ * {@link CLIP_BROKER_PACED_RATES}, and frozen below it on purpose (D621).
439
+ *
440
+ * A `stream` clip's media does not exist yet: it arrives on a socket from the
441
+ * camera while the pacer consumes it. The pacer takes `rate` seconds of media
442
+ * per second of wall clock, so the ceiling is whatever the SOURCE supplies,
443
+ * and that is a measurement per vendor rather than a preference.
444
+ *
445
+ * Reolink, the one provider on this envelope, was measured on 592
446
+ * (2026-09-23, D620): with the `<ReplaySeek>` that 0.11.1 sends before every
447
+ * replay, a sub clip arrives at **1.2×** and a main twin at **1.37×**. The
448
+ * ladder below is therefore ALREADY beyond this transport above 1 — the D612
449
+ * defect returned through a door nobody opened — and D620 fixed the repair
450
+ * order: the supply first, the ladder after. So this array does not move with
451
+ * the broker's, and it does not gain `8`; what it gains, when the library
452
+ * forwards a `<playSpeed>` large enough to feed the rate being paced over it,
453
+ * is the right to be widened on a measurement rather than shortened on one.
454
+ */
455
+ export declare const CLIP_STREAM_SOURCE_RATES: readonly number[];
456
+ /**
457
+ * The rates a REALTIME-BOUND stream can be delivered at.
458
+ *
459
+ * **Measured, and it is the whole reason this second ladder exists.** A
460
+ * Hikvision RTSP replay is not a fast fetch: it delivers the recording at the
461
+ * speed it was recorded. On 1436, 2026-09-23, one stream carried 176 access
462
+ * units in 14 274 ms of wall clock at a measured 12.5 fps — 14.08 s of media
463
+ * in 14.27 s, **1.01× realtime** — and a whole 18 s clip took 22.3 s to fetch
464
+ * end to end. Reolink's cmd-5 transfer, which {@link CLIP_BROKER_PACED_RATES}
465
+ * was written for, lands the same bytes at 49–62× and therefore has the entire
466
+ * clip in hand within the first second.
467
+ *
468
+ * The broker's pacer consumes `rate` seconds of media per second of wall
469
+ * clock while the socket supplies one. So at any rate above 1 the buffer
470
+ * drains at `(rate − 1)×` and a stream that is only seconds old runs dry
471
+ * almost at once — the viewer stops, the queue empties, and the provider's
472
+ * own drain gate eventually reports `peer-stalled`. Nothing about that is a
473
+ * bug to be fixed with a bigger buffer: there is no buffer, because the bytes
474
+ * do not exist yet.
475
+ *
476
+ * So the honest answer is the DECLARATION (D612's own rule, applied to the
477
+ * case it did not yet have): a rate this transport cannot deliver is never
478
+ * offered. Below 1 is free — a slower pacer only lets the buffer grow.
479
+ *
480
+ * When a clip is served from a FILE the constraint is gone with the transport,
481
+ * which is why `file` keeps the full ladder.
482
+ */
483
+ export declare const CLIP_REALTIME_SOURCE_RATES: readonly number[];
484
+ /**
485
+ * What a surface may DRAW for this provider's clips — the answer to
486
+ * {@link videoclipsCapability.methods.getPlaybackOptions} (D612).
487
+ *
488
+ * The envelope is a PROVIDER fact, not a clip fact, and that is measured, not
489
+ * assumed: the broker's `chooseClipPath` reads exactly two inputs — whether
490
+ * `dialClipStream` and `readClipBytes` are wired — and both are constants of
491
+ * the broker's own closure over the provider's methods. The `profile` it is
492
+ * handed is explicitly not read. So every clip of a provider is served the
493
+ * same way, and a per-clip channel carried a value that could not vary. The
494
+ * per-clip `clipTransport` server message was removed for exactly that reason.
495
+ *
496
+ * Queried per camera, before a clip is picked, so a control is rendered or
497
+ * DISABLED rather than offered and refused at play time (D62: a disabled
498
+ * control reads as unavailable, one that undoes the gesture reads as broken).
499
+ */
500
+ export declare const ClipPlaybackOptionsSchema: z.ZodObject<{
501
+ transport: z.ZodEnum<{
502
+ file: "file";
503
+ stream: "stream";
504
+ }>;
505
+ seek: z.ZodEnum<{
506
+ forward: "forward";
507
+ free: "free";
508
+ }>;
509
+ stepBack: z.ZodBoolean;
510
+ scrub: z.ZodBoolean;
511
+ rates: z.ZodReadonly<z.ZodArray<z.ZodNumber>>;
512
+ }, z.core.$strip>;
513
+ export type ClipPlaybackOptions = z.infer<typeof ClipPlaybackOptionsSchema>;
341
514
  export declare const ClipSourceAvailabilitySchema: z.ZodObject<{
342
515
  state: z.ZodEnum<{
343
516
  ok: "ok";
@@ -533,14 +706,18 @@ export declare const videoclipsCapability: {
533
706
  *
534
707
  * `getClipPlayback` is the right answer for a player: it hands back a URL
535
708
  * on a plane the hub serves `access:'authenticated'`, which a browser and a
536
- * viewer session satisfy. It is the wrong answer for another ADDON. There
537
- * is no addon→addon byte transport in this framework — `AddonDataPlane`
538
- * only lets an addon SERVE, on `127.0.0.1` behind a per-listener secret
539
- * only the hub may present — so a recorder that wants a camera's clip
540
- * cannot fetch that URL. This method is the one seam that exists for it,
541
- * and it is deliberately the same shape (and the same bound) as
542
- * `recordingExport.readExportBytes`, which exists for the mirror-image
543
- * reason.
709
+ * viewer session satisfy. It is the wrong answer for another ADDON: the
710
+ * hub's proxy in front of that plane takes only a user credential, which
711
+ * an addon does not hold, so a recorder that wants a camera's clip cannot
712
+ * fetch that URL. This method is the shape that answer forced — the same
713
+ * one (and the same bound) as `recordingExport.readExportBytes`.
714
+ *
715
+ * **It is no longer the only seam.** Until D613 there was no addon→addon
716
+ * byte transport at all; there is now
717
+ * ({@link offerClipBytes}, over `ctx.peerBytes`), it holds nothing on
718
+ * either side, and it is what a clip EXPORT uses. This method remains for
719
+ * a consumer that genuinely wants the bytes in hand, and as the named
720
+ * fallback for a provider not yet redeployed.
544
721
  *
545
722
  * Routing needs no `provider` pin: the id is source-prefixed and
546
723
  * self-contained, so `device-collection-dispatch.ts` rule 3 hands the call
@@ -588,6 +765,72 @@ export declare const videoclipsCapability: {
588
765
  }, z.core.$strip>, "query"> & {
589
766
  readonly providerOptional: true;
590
767
  };
768
+ /**
769
+ * Where this clip's finished bytes can be TAKEN — the by-handle read a
770
+ * clip EXPORT pulls, over the addon→addon byte transport (D613).
771
+ *
772
+ * This is {@link readClipBytes} with the envelope removed. Same gates,
773
+ * same vocabulary, same completion rules, same `served` contract — the
774
+ * only difference is that the bytes travel over a one-shot loopback
775
+ * socket instead of inside a base64 field, so neither side holds the
776
+ * payload whole and the 50 MiB refusal on a long `high` twin stops
777
+ * existing. The bound that remains is
778
+ * {@link VIDEOCLIPS_MAX_OFFER_BYTES}, and it bounds the PRODUCER's own
779
+ * copy rather than the transport.
780
+ *
781
+ * **The ticket is loopback and same-host.** A provider on an agent mints a
782
+ * URL that means nothing on the hub, and `ctx.peerBytes.open` refuses it
783
+ * `cross-node` by name rather than dialling whatever else holds that port
784
+ * here. A consumer that can be on the other side of a node boundary from
785
+ * its provider must be able to read that refusal and say so; it must not
786
+ * treat it as "no bytes".
787
+ *
788
+ * **A ticket is a one-shot bearer credential with a seconds-long life.**
789
+ * Take it immediately, never persist it, never log its `url`. An untaken
790
+ * ticket costs the provider one map entry until its TTL, and outstanding
791
+ * tickets are bounded — which is also what makes a per-frame misuse of
792
+ * this method refuse by name rather than work slowly (D9/D18: this is a
793
+ * by-handle fetch of finished media, not a frame pipe).
794
+ *
795
+ * Optional on the provider for the same reason `readClipBytes` is: a
796
+ * source with no camera socket behind it has no bytes. A provider that
797
+ * predates this method answers `NOT_IMPLEMENTED`, and a consumer may fall
798
+ * back to `readClipBytes` — but it says so in the log, with the deploy
799
+ * hint, because that fallback re-imposes the 50 MiB refusal and an
800
+ * operator who sees `too-large-to-transfer` after this shipped is looking
801
+ * at a stale addon, not at a clip that cannot be exported.
802
+ */
803
+ readonly offerClipBytes: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
804
+ deviceId: z.ZodNumber;
805
+ clipId: z.ZodString;
806
+ provider: z.ZodString;
807
+ profile: z.ZodOptional<z.ZodEnum<{
808
+ high: "high";
809
+ mid: "mid";
810
+ low: "low";
811
+ }>>;
812
+ maxBytes: z.ZodOptional<z.ZodNumber>;
813
+ wake: z.ZodOptional<z.ZodEnum<{
814
+ authorised: "authorised";
815
+ }>>;
816
+ }, z.core.$strip>, z.ZodObject<{
817
+ ticket: z.ZodObject<{
818
+ url: z.ZodString;
819
+ hostNodeId: z.ZodString;
820
+ expiresAtMs: z.ZodNumber;
821
+ declaredBytes: z.ZodNullable<z.ZodNumber>;
822
+ }, z.core.$strip>;
823
+ contentType: z.ZodString;
824
+ name: z.ZodString;
825
+ served: z.ZodEnum<{
826
+ high: "high";
827
+ mid: "mid";
828
+ low: "low";
829
+ }>;
830
+ durationMs: z.ZodOptional<z.ZodNumber>;
831
+ }, z.core.$strip>, "query"> & {
832
+ readonly providerOptional: true;
833
+ };
591
834
  /**
592
835
  * Where this clip's STREAM can be dialled (D597) — the forward-only
593
836
  * fMP4 the provider writes from the first muxed byte, for the broker to
@@ -639,6 +882,92 @@ export declare const videoclipsCapability: {
639
882
  }, z.core.$strip>, "query"> & {
640
883
  readonly providerOptional: true;
641
884
  };
885
+ /**
886
+ * What a surface may DRAW for this provider's clips: the rates it can be
887
+ * played at, whether scrub is served, how far a position may be moved,
888
+ * and whether a backward frame-step means anything (D612).
889
+ *
890
+ * **The only authority.** The per-clip `clipTransport` server message
891
+ * that used to carry the same answer was removed: the broker's transport
892
+ * choice reads nothing that varies per clip, so the clip level had no
893
+ * information the provider does not already have, and two channels that
894
+ * can disagree are worse than one (D62).
895
+ *
896
+ * Asked per camera and per provider, so it must stay CHEAP — it is a
897
+ * statement about wiring, answered from a constant, never a call to the
898
+ * camera. A provider answers with one of {@link CLIP_PLAYBACK_OPTIONS}
899
+ * and never composes an envelope of its own.
900
+ *
901
+ * Optional, and absence is load-bearing: a provider that has not answered
902
+ * has not restricted anything, and a viewer reads it as the freedom it
903
+ * always had. See D612 on the rollout order that absence implies.
904
+ */
905
+ readonly getPlaybackOptions: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
906
+ deviceId: z.ZodNumber;
907
+ provider: z.ZodString;
908
+ }, z.core.$strip>, z.ZodObject<{
909
+ transport: z.ZodEnum<{
910
+ file: "file";
911
+ stream: "stream";
912
+ }>;
913
+ seek: z.ZodEnum<{
914
+ forward: "forward";
915
+ free: "free";
916
+ }>;
917
+ stepBack: z.ZodBoolean;
918
+ scrub: z.ZodBoolean;
919
+ rates: z.ZodReadonly<z.ZodArray<z.ZodNumber>>;
920
+ }, z.core.$strip>, "query"> & {
921
+ readonly providerOptional: true;
922
+ };
642
923
  };
643
924
  };
644
925
  export type IVideoclipsProvider = InferProvider<typeof videoclipsCapability>;
926
+ /**
927
+ * NOTE ON PLACEMENT: this block lives AFTER the capability definition on
928
+ * purpose. `scripts/lib/parse-cap.ts` finds a cap by the FIRST
929
+ * `export const <X> = {` in the file, so an object literal declared above
930
+ * `videoclipsCapability` is silently taken for the capability itself and
931
+ * codegen emits a `DeviceProxy` entry that does not type-check. Found the
932
+ * hard way, 2026-09-23.
933
+ */
934
+ /**
935
+ * The two envelopes, spelled ONCE.
936
+ *
937
+ * A provider answers with the entry for the transport its own wiring buys —
938
+ * `stream` if it implements `dialClipStream`, `file` if it implements only
939
+ * `readClipBytes` — and never composes one of its own. One table is what
940
+ * makes "the provider may not promise what no clip can be given" structural
941
+ * instead of a discipline: there is nothing else to promise.
942
+ */
943
+ export declare const CLIP_PLAYBACK_OPTIONS: {
944
+ readonly stream: {
945
+ readonly transport: "stream";
946
+ readonly seek: "forward";
947
+ readonly stepBack: false;
948
+ readonly scrub: false;
949
+ readonly rates: readonly number[];
950
+ };
951
+ /**
952
+ * A `stream` whose SOURCE runs at realtime — a Hikvision RTSP replay.
953
+ *
954
+ * Same verbs as `stream`, a shorter rate ladder, and the difference is a
955
+ * measurement rather than a preference: see
956
+ * {@link CLIP_REALTIME_SOURCE_RATES}. A third entry rather than a parameter,
957
+ * because a provider must still be able to do nothing but NAME one of these.
958
+ */
959
+ readonly realtimeStream: {
960
+ readonly transport: "stream";
961
+ readonly seek: "forward";
962
+ readonly stepBack: false;
963
+ readonly scrub: false;
964
+ readonly rates: readonly number[];
965
+ };
966
+ readonly file: {
967
+ readonly transport: "file";
968
+ readonly seek: "free";
969
+ readonly stepBack: true;
970
+ readonly scrub: true;
971
+ readonly rates: readonly number[];
972
+ };
973
+ };