@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.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/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- package/dist/transport/index.js.map +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type Logger } from "../../core/logger.js";
|
|
2
|
+
import type { P2PSession } from "./p2p-session.js";
|
|
3
|
+
/** A station's power tier — governs its idle window. */
|
|
4
|
+
export type PowerTier = "wired" | "battery";
|
|
5
|
+
/**
|
|
6
|
+
* A station open was abandoned because its entry was closed or superseded while the factory ran.
|
|
7
|
+
*
|
|
8
|
+
* Distinct from a connect failure: nothing is wrong with the device, the caller's reason to open it
|
|
9
|
+
* simply stopped applying. A speculative caller treats this as a non-event; anyone who asked for the
|
|
10
|
+
* session on a caller's behalf must still surface it.
|
|
11
|
+
*/
|
|
12
|
+
export declare class SessionSupersededError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/** Default idle window for a battery station before its session is closed to let the device sleep. */
|
|
15
|
+
export declare const BATTERY_IDLE_MS = 300000;
|
|
16
|
+
/** How long a single control command holds a session warm after dispatch (a burst keeps re-holding). */
|
|
17
|
+
export declare const COMMAND_KEEPALIVE_MS = 15000;
|
|
18
|
+
/**
|
|
19
|
+
* Default window a speculative pre-warm (e.g. after a doorbell ring) holds its user for. Expiring
|
|
20
|
+
* releases that user; it does not close the session — the station's own idle window then runs, so an
|
|
21
|
+
* unattended pre-warm on a battery station costs this plus {@link BATTERY_IDLE_MS}.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PREWARM_MS = 28000;
|
|
24
|
+
export interface SessionManagerOpts {
|
|
25
|
+
/** Idle window for battery stations (ms). Default {@link BATTERY_IDLE_MS}. */
|
|
26
|
+
batteryIdleMs?: number;
|
|
27
|
+
/** Keepalive a single command holds after dispatch (ms). Default {@link COMMAND_KEEPALIVE_MS}. */
|
|
28
|
+
commandKeepAliveMs?: number;
|
|
29
|
+
/** Power tier per station serial — injected by the facade (no model import). Default: everything `wired`. */
|
|
30
|
+
poweredFor?: (parentSn: string) => PowerTier;
|
|
31
|
+
/**
|
|
32
|
+
* Called after the manager closes a station on its OWN initiative — an elapsed idle window, or a
|
|
33
|
+
* deferred reset falling due.
|
|
34
|
+
*
|
|
35
|
+
* Those two are the only closes with no caller to follow up: everything riding the session is stale the
|
|
36
|
+
* moment it goes, and only the owner knows what that is. A close a caller asked for is that caller's to
|
|
37
|
+
* clean up after, which is why this does not fire for {@link SessionManager.close},
|
|
38
|
+
* {@link SessionManager.closeAll}, or a superseded open.
|
|
39
|
+
*/
|
|
40
|
+
onAutoClose?: (parentSn: string) => void;
|
|
41
|
+
/** Diagnostics sink for the lifecycle transitions (open / idle-arm / detach). Omit for silence. */
|
|
42
|
+
logger?: Logger;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Manages P2P sessions keyed by **parent station serial**. The router builds/wires the actual
|
|
46
|
+
* `P2PSession` (it owns the socket + event fan-out); this decides open/close timing.
|
|
47
|
+
*/
|
|
48
|
+
export declare class SessionManager {
|
|
49
|
+
private readonly opts;
|
|
50
|
+
private readonly entries;
|
|
51
|
+
/** Invalidates station factories that finish after {@link closeAll}. */
|
|
52
|
+
private generation;
|
|
53
|
+
private readonly logger;
|
|
54
|
+
constructor(opts?: SessionManagerOpts);
|
|
55
|
+
/** The live session for a station, or `undefined` if not open. */
|
|
56
|
+
get(parentSn: string): P2PSession | undefined;
|
|
57
|
+
/** Serials of stations with a live session. */
|
|
58
|
+
keys(): string[];
|
|
59
|
+
/** A plain `Map<parentSn, P2PSession>` snapshot of the live sessions (for `getSessions()` / tests). */
|
|
60
|
+
liveSessions(): Map<string, P2PSession>;
|
|
61
|
+
/** Get or create the lifecycle entry for a station. */
|
|
62
|
+
private entry;
|
|
63
|
+
/** Register an already-built session for test seeding or an externally assembled connection. */
|
|
64
|
+
register(parentSn: string, session: P2PSession): void;
|
|
65
|
+
/**
|
|
66
|
+
* Ensure a session to `parentSn` is open, building it via `factory` if cold. Concurrent calls for the
|
|
67
|
+
* same cold station share ONE connect (the `connecting` promise); `factory` builds + wires + awaits
|
|
68
|
+
* `connect()` and resolves the connected session.
|
|
69
|
+
*/
|
|
70
|
+
acquire(parentSn: string, factory: (register: (session: P2PSession) => void) => Promise<P2PSession>): Promise<P2PSession>;
|
|
71
|
+
/** Add a reason to stay connected; cancels a pending idle-close. */
|
|
72
|
+
retain(parentSn: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Release a reason; arm the idle-close when the last one goes.
|
|
75
|
+
*
|
|
76
|
+
* A release with nothing retained is REFUSED rather than clamped to zero. Such a release was never
|
|
77
|
+
* earned on this entry, and letting it proceed would either restart a battery station's idle window
|
|
78
|
+
* from scratch or complete a deferred reset a real viewer has not yet earned. Clamping to zero did
|
|
79
|
+
* both silently.
|
|
80
|
+
*/
|
|
81
|
+
release(parentSn: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Hold a session warm for `commandKeepAliveMs` after a control command, then release. A burst of
|
|
84
|
+
* commands each re-holds before the previous release fires, so the session never idles mid-burst.
|
|
85
|
+
*/
|
|
86
|
+
bumpCommand(parentSn: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Retain a station and release it again after `ms` — the primitive behind command-keepalive and event
|
|
89
|
+
* pre-warm, and the only way to hold one open without an attachment to release it.
|
|
90
|
+
*
|
|
91
|
+
* The timer is owned by the entry, so {@link discard} cancels it. That ownership is the point: keyed
|
|
92
|
+
* only by serial, an expiring hold would otherwise outlive the entry it was taken on and release a
|
|
93
|
+
* retain counted by the SUCCESSOR entry — dropping a live viewer's count and arming an idle-detach
|
|
94
|
+
* underneath it.
|
|
95
|
+
*/
|
|
96
|
+
hold(parentSn: string, ms: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* Arm the idle-close timer for a station whose retain count just reached zero. A wired station with
|
|
99
|
+
* an infinite window is left persistent (no timer). Any subsequent {@link retain} cancels it.
|
|
100
|
+
*/
|
|
101
|
+
private armIdle;
|
|
102
|
+
/**
|
|
103
|
+
* Close a station's session once its idle window elapses with nothing retained, letting the device sleep.
|
|
104
|
+
* Re-checks the count first (activity between the timer firing and now re-arms instead). Dropping the
|
|
105
|
+
* entry here and the session's own `close` → {@link remove} are both idempotent.
|
|
106
|
+
*/
|
|
107
|
+
private onIdle;
|
|
108
|
+
/**
|
|
109
|
+
* Close a station the manager itself decided to close, and announce it.
|
|
110
|
+
*
|
|
111
|
+
* The announcement is the whole point: {@link SessionManagerOpts.onAutoClose} is how the owner learns
|
|
112
|
+
* about a teardown it did not request, and so the only way anything riding the session — a lingering
|
|
113
|
+
* live source, a talkback — gets dropped rather than handed out again over a dead connection.
|
|
114
|
+
*
|
|
115
|
+
* It fires the moment the entry is discarded, BEFORE the socket teardown and before any deferred reset
|
|
116
|
+
* settles. That is deliberate on both counts: from the instant the entry is gone a fresh acquisition
|
|
117
|
+
* resolves a new session while a cached source still points at the old one, so announcing later leaves
|
|
118
|
+
* a window in which a viewer can attach to a stale source; and a caller awaiting a reset should find
|
|
119
|
+
* the station's riders already dropped when it resumes.
|
|
120
|
+
*
|
|
121
|
+
* An entry that never carried a session is still torn down, but silently: a pre-warm whose open failed
|
|
122
|
+
* leaves one behind, and announcing it would report a station closed that was never reported open.
|
|
123
|
+
*/
|
|
124
|
+
private autoClose;
|
|
125
|
+
/** Drop a station's entry + timer (called from the session's `close` handler). Idempotent. */
|
|
126
|
+
remove(parentSn: string): void;
|
|
127
|
+
/** Close one station now and discard its lifecycle entry. */
|
|
128
|
+
close(parentSn: string): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Reset once every viewer detaches, ignoring only expiring holds.
|
|
131
|
+
*
|
|
132
|
+
* Every caller that arrives while one is already pending gets the SAME promise: the outcome is a
|
|
133
|
+
* property of the station's teardown, not of who asked, so one deferred per entry is the whole
|
|
134
|
+
* mechanism — and it cannot grow with the number of callers.
|
|
135
|
+
*
|
|
136
|
+
* Both branches close through {@link autoClose}: the caller asked for a recycle, not for the station's
|
|
137
|
+
* live sources to be dropped, so it does not clean up after one — exactly like the idle path.
|
|
138
|
+
*/
|
|
139
|
+
resetWhenUnused(parentSn: string): Promise<void>;
|
|
140
|
+
/** Settle a discarded entry's reset callers with the same outcome as its session close. */
|
|
141
|
+
private settleReset;
|
|
142
|
+
/** Close one discarded entry and settle only its own reset callers before preserving any failure. */
|
|
143
|
+
private closeEntry;
|
|
144
|
+
/**
|
|
145
|
+
* Discard one lifecycle entry and return it for bounded close/reset completion.
|
|
146
|
+
*
|
|
147
|
+
* Every timer the entry owns dies with it — the idle window and any hold still counting down. A
|
|
148
|
+
* discarded entry owns no live timer, which is what stops a deferred release from landing on whatever
|
|
149
|
+
* entry next occupies this serial.
|
|
150
|
+
*/
|
|
151
|
+
private discard;
|
|
152
|
+
/** Close every session and clear all timers. */
|
|
153
|
+
closeAll(): Promise<void>;
|
|
154
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { type Logger } from "../../core/logger.js";
|
|
2
|
+
import { type ParamSets } from "./annexb.js";
|
|
3
|
+
import type { LiveAudioFrame, LiveStreamConsumer, LiveStreamHandle, LiveVideoConfig, LiveVideoFrame } from "../../core/contracts.js";
|
|
4
|
+
/** Lifecycle state of a {@link SharedLiveSource}. */
|
|
5
|
+
export type SharedLiveState = "idle" | "warming" | "live" | "lingering" | "stopped";
|
|
6
|
+
export interface SharedLiveSourceOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Factory that builds a fresh, **un-started** {@link LiveStreamHandle}. Called on every (re)warm so
|
|
9
|
+
* a reconnect rebuilds the stream rather than reusing a dead one. `SharedLiveSource` calls
|
|
10
|
+
* `.start()` itself.
|
|
11
|
+
*
|
|
12
|
+
* `ctx.reassertWanted` answers whether this pull still has anyone attached. A stream that re-asserts a
|
|
13
|
+
* channel to hold it open should consult it, so a pull nothing is watching stops competing for a station
|
|
14
|
+
* that serves one camera at a time.
|
|
15
|
+
*/
|
|
16
|
+
makeStream: (ctx: {
|
|
17
|
+
reassertWanted: () => boolean;
|
|
18
|
+
}) => LiveStreamHandle;
|
|
19
|
+
/** No-consumer grace before teardown (default 8000ms). Distinct from the stream's keepalive. */
|
|
20
|
+
lingerMs?: number;
|
|
21
|
+
/** Per-consumer bounded queue depth; overflow → drop-to-keyframe (default 900 ≈ 30s @ 30fps). */
|
|
22
|
+
maxQueue?: number;
|
|
23
|
+
/** Rolling prebuffer window in seconds, 0 = off (default 0). */
|
|
24
|
+
preBufferSeconds?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Warm-up start retry interval (default 2000ms). After warming, if no keyframe has arrived, the source
|
|
27
|
+
* re-issues the start ({@link LiveStreamHandle.nudge}) every interval — self-healing a start that
|
|
28
|
+
* raced the level-2 key negotiation, independent of any caller keepalive.
|
|
29
|
+
*/
|
|
30
|
+
warmRetryMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Warm-up deadline (default 20000ms). If no keyframe arrives within it, the source emits `error` to
|
|
33
|
+
* consumers ("failed to start") and tears down, so `live()` never hangs silently on a dead start.
|
|
34
|
+
*/
|
|
35
|
+
warmTimeoutMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Power source, a runtime device fact (`"battery"` incl. solar, or `"wired"`) — NOT a device family
|
|
38
|
+
* trait; the model derives it from the resolved capability set and passes it through. `"wired"`
|
|
39
|
+
* (default) streams unbounded; `"battery"` bounds a continuous stream to {@link batteryBudgetMs}.
|
|
40
|
+
*/
|
|
41
|
+
powered?: "wired" | "battery";
|
|
42
|
+
/** Battery/solar continuous-stream budget before the `budget` notice fires (default 45000ms). */
|
|
43
|
+
batteryBudgetMs?: number;
|
|
44
|
+
/** Grace after the budget notice to call `extend()` before the source auto-stops (default 10000ms). */
|
|
45
|
+
budgetGraceMs?: number;
|
|
46
|
+
/** Diagnostics sink. Omit for silence. */
|
|
47
|
+
logger?: Logger;
|
|
48
|
+
/** Prefix label for log lines (e.g. the `parentSn:channel` key), for multi-source disambiguation. */
|
|
49
|
+
label?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Called when the FIRST consumer attaches (0→1). The router uses this to register the source as a
|
|
52
|
+
* "user" of the station's P2P session (so an active stream cancels the session's idle-detach). Paired
|
|
53
|
+
* with {@link onIdle}. Optional — omit if the caller doesn't manage session lifecycle.
|
|
54
|
+
*/
|
|
55
|
+
onActive?: () => void;
|
|
56
|
+
/** Called when the LAST consumer detaches (1→0) — the router releases its session user. See {@link onActive}. */
|
|
57
|
+
onIdle?: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Called when a stream is torn down having **never delivered a keyframe**, AFTER consumers have been told.
|
|
60
|
+
*
|
|
61
|
+
* A source can only rebuild its stream; it holds a factory, not the session that stream rides on. When
|
|
62
|
+
* the session — or the per-device state carried on it — is what has stopped serving this device, every
|
|
63
|
+
* rebuild starts another stream over the same session and dies the same way, so the owner of the session
|
|
64
|
+
* has to hear about it to do anything else.
|
|
65
|
+
*
|
|
66
|
+
* The condition is deliberately "no keyframe ever arrived", not "the warm-up deadline fired". A start can
|
|
67
|
+
* fail without that deadline being reached — an upstream error or stop can arrive first, the battery
|
|
68
|
+
* budget can stop the pull, and a caller that gives up before the deadline cancels it on the way out
|
|
69
|
+
* (`clearWarmWatch`) — and all of those are the same dead start. Enumerating the ways instead of naming
|
|
70
|
+
* the condition is how the case that actually happens gets left out.
|
|
71
|
+
*
|
|
72
|
+
* Not called by {@link SharedLiveSource.dispose}: the owner asked for that one, and it is the very thing
|
|
73
|
+
* an owner does in response to this callback.
|
|
74
|
+
*/
|
|
75
|
+
onStartFailed?: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* A media start was abandoned unacknowledged before anything was delivered, so this session is not being
|
|
78
|
+
* heard. The owner is asked for a replacement and calls {@link SharedLiveSource.rewarm} once it has one.
|
|
79
|
+
*
|
|
80
|
+
* Asked at most once per warm-up: further abandonments are the same session saying the same thing.
|
|
81
|
+
*/
|
|
82
|
+
onSessionUnreachable?: () => void;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A single consumer of a {@link SharedLiveSource}. A {@link LiveStreamConsumer} (so `live()` can hand it
|
|
86
|
+
* back directly), plus the listener removal and arrival-timed feed the recording and readable egresses use.
|
|
87
|
+
*/
|
|
88
|
+
export interface Consumer extends LiveStreamConsumer {
|
|
89
|
+
/** What this consumer holds the pull for. */
|
|
90
|
+
/** Detach a previously registered listener (mirrors {@link LiveStreamHandle.on}). */
|
|
91
|
+
off(event: "video", listener: (frame: LiveVideoFrame) => void): this;
|
|
92
|
+
off(event: "audio", listener: (frame: LiveAudioFrame) => void): this;
|
|
93
|
+
off(event: "start" | "stop", listener: () => void): this;
|
|
94
|
+
off(event: "error", listener: (err: Error) => void): this;
|
|
95
|
+
/** Subscribe to frames carrying the source-captured arrival time used by the prebuffer. */
|
|
96
|
+
onMedia(listener: (item: TimedMediaFrame) => void): this;
|
|
97
|
+
/** True once the source has replayed a cached keyframe to this consumer (no GOP wait on join). */
|
|
98
|
+
readonly primed: boolean;
|
|
99
|
+
/** Leave the source (refcount--). Idempotent. `stop()` is an alias (LiveStreamHandle). */
|
|
100
|
+
detach(): void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One media frame retained with its transport-arrival time for prebuffer continuity.
|
|
104
|
+
*
|
|
105
|
+
* A video frame carries the coded configuration in force when it ARRIVED, because the item outlives that
|
|
106
|
+
* moment: it is the unit a keyframe-prime replays to a consumer that joined later and the unit a prebuffer
|
|
107
|
+
* drain hands over, and both have to announce the configuration their media was coded under rather than
|
|
108
|
+
* whichever one is current by the time they are delivered.
|
|
109
|
+
*/
|
|
110
|
+
export type TimedMediaFrame = {
|
|
111
|
+
kind: "video";
|
|
112
|
+
frame: LiveVideoFrame;
|
|
113
|
+
timestampMs: number;
|
|
114
|
+
config: LiveVideoConfig;
|
|
115
|
+
} | {
|
|
116
|
+
kind: "audio";
|
|
117
|
+
frame: LiveAudioFrame;
|
|
118
|
+
timestampMs: number;
|
|
119
|
+
};
|
|
120
|
+
export declare class SharedLiveSource {
|
|
121
|
+
private readonly opts;
|
|
122
|
+
private stream?;
|
|
123
|
+
private readonly consumers;
|
|
124
|
+
/** No-consumer teardown grace (arm/cancel on the last-detach / re-attach transition). */
|
|
125
|
+
private readonly lingerTimer;
|
|
126
|
+
private _state;
|
|
127
|
+
private disposed;
|
|
128
|
+
/**
|
|
129
|
+
* What the CURRENT stream generation has delivered — replaced wholesale by every {@link warm}, so a new
|
|
130
|
+
* generation cannot inherit a previous one's evidence and no field can be forgotten in the reset.
|
|
131
|
+
*
|
|
132
|
+
* All three are read together to stage a start failure: `keyframe` is what makes a stream live at all,
|
|
133
|
+
* while `video` and `audio` are what separate a source that produced nothing from one whose units were
|
|
134
|
+
* never decodable and one that is answering with sound and no picture — the three stages of
|
|
135
|
+
* {@link LiveStreamStartError}.
|
|
136
|
+
*/
|
|
137
|
+
private delivered;
|
|
138
|
+
/** Last keyframe access unit seen — replayed to a joining consumer (keyframe-prime). */
|
|
139
|
+
private lastKeyframe?;
|
|
140
|
+
/** Last parameter sets the stream announced — see {@link parameterSets}. */
|
|
141
|
+
private lastParamSets?;
|
|
142
|
+
/**
|
|
143
|
+
* The geometry the parameter sets in force state, and the sets it was read from.
|
|
144
|
+
*
|
|
145
|
+
* Holding the sets it came from is what keeps the read to one per announcement: `updatedParamSets`
|
|
146
|
+
* returns the SAME object when a frame announces nothing, so identity says the geometry cannot have
|
|
147
|
+
* moved without comparing any bytes.
|
|
148
|
+
*/
|
|
149
|
+
private declaredGeometry?;
|
|
150
|
+
private configuredFrom?;
|
|
151
|
+
/** Rolling prebuffer, keyframe-alignable on drain. */
|
|
152
|
+
private ring;
|
|
153
|
+
/** Warm-up start-retry ticker (interval) + single-shot deadline; cleared once the first keyframe arrives. */
|
|
154
|
+
private warmRetryTimer?;
|
|
155
|
+
/** Whether this warm-up has already asked its owner to replace the session. */
|
|
156
|
+
private sessionReplacementAsked;
|
|
157
|
+
/**
|
|
158
|
+
* Whether the pending watch is a REUSE watch, which any frame settles.
|
|
159
|
+
*
|
|
160
|
+
* A cold warm-up needs a keyframe: nothing can be decoded without one. A join already holds the retained
|
|
161
|
+
* keyframe, so what its watch is missing is evidence the stream is still being served — and a delta frame is
|
|
162
|
+
* that evidence. Requiring a keyframe there let the deadline outlive an actively delivering stream whose
|
|
163
|
+
* group of pictures is longer than the window, and the timeout fails EVERY consumer.
|
|
164
|
+
*/
|
|
165
|
+
private reuseWatch;
|
|
166
|
+
/** This source's opaque handle for tracing — see {@link SharedLiveSource.trace}. */
|
|
167
|
+
private readonly traceId;
|
|
168
|
+
/**
|
|
169
|
+
* How many re-issues this watch has spent with nothing arriving since it was armed.
|
|
170
|
+
*
|
|
171
|
+
* What a stream delivered BEFORE the current watch is no evidence about now — a reused stream's upstream may
|
|
172
|
+
* have served plenty and since been dropped by the station, and the retained keyframe replayed to a joining
|
|
173
|
+
* consumer says nothing either. Only a frame arriving after the watch was armed does, and {@link delivered}
|
|
174
|
+
* is reset to track exactly that.
|
|
175
|
+
*
|
|
176
|
+
* The first re-issue is therefore a keepalive: a reuse cannot yet know which case it is in, and a keepalive
|
|
177
|
+
* is right where the station is still serving and harmless where it is not. A second one due with nothing
|
|
178
|
+
* arrived is the answer — no bound of its own, the retry's own cadence.
|
|
179
|
+
*/
|
|
180
|
+
private fruitlessReissues;
|
|
181
|
+
private readonly warmDeadlineTimer;
|
|
182
|
+
private warmAttempts;
|
|
183
|
+
/** Battery budget timer + post-notice grace timer (battery/solar sources only). */
|
|
184
|
+
private readonly budgetTimer;
|
|
185
|
+
private readonly budgetGraceTimer;
|
|
186
|
+
private readonly lingerMs;
|
|
187
|
+
private readonly maxQueue;
|
|
188
|
+
private readonly preBufferMs;
|
|
189
|
+
private readonly warmRetryMs;
|
|
190
|
+
private readonly warmTimeoutMs;
|
|
191
|
+
private readonly powered;
|
|
192
|
+
private readonly batteryBudgetMs;
|
|
193
|
+
private readonly budgetGraceMs;
|
|
194
|
+
private readonly logger;
|
|
195
|
+
private readonly tag;
|
|
196
|
+
constructor(opts: SharedLiveSourceOptions);
|
|
197
|
+
get state(): SharedLiveState;
|
|
198
|
+
get consumerCount(): number;
|
|
199
|
+
/**
|
|
200
|
+
* The parameter sets (SPS/PPS, plus VPS for H.265) most recently announced on this stream, or
|
|
201
|
+
* `undefined` before any have been seen.
|
|
202
|
+
*
|
|
203
|
+
* A camera commonly sends them ONCE, with the first keyframe of a stream. Every later access unit is
|
|
204
|
+
* then undecodable in isolation, so a consumer that collects a burst — and cannot see frames from
|
|
205
|
+
* before it joined — has no way to recover them. This source watches every frame from stream start,
|
|
206
|
+
* which makes it the only holder of the answer. A caller re-emits them ahead of its collected burst.
|
|
207
|
+
*
|
|
208
|
+
* Cleared when the stream is torn down, so a rebuilt stream never primes a burst with a dead stream's sets.
|
|
209
|
+
*/
|
|
210
|
+
get parameterSets(): ParamSets | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Attach a new consumer. Warms the stream on the first attach (or cancels a pending linger teardown
|
|
213
|
+
* and reuses the warm stream), then replays the cached keyframe so the consumer can decode at once.
|
|
214
|
+
*/
|
|
215
|
+
attach(): Consumer;
|
|
216
|
+
/**
|
|
217
|
+
* Attach at the same instant a keyframe-aligned prebuffer snapshot is taken. The returned consumer
|
|
218
|
+
* is not separately keyframe-primed, so replaying `buffered` followed by its live events neither
|
|
219
|
+
* duplicates the newest IDR nor leaves a gap at the handoff.
|
|
220
|
+
*/
|
|
221
|
+
attachWithPrebuffer(seconds: number): {
|
|
222
|
+
consumer: Consumer;
|
|
223
|
+
buffered: TimedMediaFrame[];
|
|
224
|
+
};
|
|
225
|
+
private attachConsumer;
|
|
226
|
+
/**
|
|
227
|
+
* Emit a live trace under this source's opaque handle — `pull-N` by order of construction in this process.
|
|
228
|
+
*
|
|
229
|
+
* Not {@link SharedLiveSourceOptions.label}, which is the router's `stationSn:channel` key: that is a serial,
|
|
230
|
+
* and a serial in a retained record survives every redaction a host applies.
|
|
231
|
+
*/
|
|
232
|
+
private trace;
|
|
233
|
+
/**
|
|
234
|
+
* Build the underlying stream, wire its frames into the fan-out, and start it.
|
|
235
|
+
*
|
|
236
|
+
* Every warm goes through here, so a source that is rebuilt on a replacement session listens on exactly
|
|
237
|
+
* the events the first attempt did.
|
|
238
|
+
*/
|
|
239
|
+
private openStream;
|
|
240
|
+
/**
|
|
241
|
+
* Build + start the underlying stream, wire its frames into the fan-out, and watch the warm-up.
|
|
242
|
+
*/
|
|
243
|
+
private warm;
|
|
244
|
+
/**
|
|
245
|
+
* A start was abandoned unacknowledged. Ask for a replacement session where nothing has been delivered yet.
|
|
246
|
+
*
|
|
247
|
+
* The abandonment is roughly twenty byte-identical sends with no reply, against acknowledgement latencies of
|
|
248
|
+
* 4–37 ms awake and 238 ms waking, so it is the session that is not being heard rather than a slow device —
|
|
249
|
+
* `P2PSession` says as much: the camera was never told to stream, so this warm-up can only time out. Where
|
|
250
|
+
* media has already flowed the abandonment means something else and this does nothing.
|
|
251
|
+
*
|
|
252
|
+
* The retry ticker is stopped while a replacement is awaited, because every tick it issues goes to the same
|
|
253
|
+
* unheard session. The DEADLINE is left running: the window belongs to the attempt, not to the session it
|
|
254
|
+
* started on.
|
|
255
|
+
*/
|
|
256
|
+
private onStartUnacknowledged;
|
|
257
|
+
/**
|
|
258
|
+
* Warm again on a session the owner has replaced, inside the deadline the first attempt started.
|
|
259
|
+
*
|
|
260
|
+
* The previous stream is dropped rather than stopped through the state machine: it speaks to a session that
|
|
261
|
+
* is gone, and its `stop` would be read as an upstream end. Only a warm-up that asked for a replacement
|
|
262
|
+
* rewarms, so this is inert on a source that is streaming or has already failed.
|
|
263
|
+
*/
|
|
264
|
+
rewarm(): void;
|
|
265
|
+
/**
|
|
266
|
+
* Arm the deadline a stream must deliver within, and the ticker that re-issues its start until it does.
|
|
267
|
+
*
|
|
268
|
+
* The deadline is armed before the ticker so that a retry falling on the same instant as the deadline is
|
|
269
|
+
* never issued, which keeps `attempts` on {@link LiveStreamStartError} equal to the number of media starts
|
|
270
|
+
* actually sent. A stream with no `nudge` cannot be retried, so its watch stays at one attempt however long
|
|
271
|
+
* the deadline is.
|
|
272
|
+
*/
|
|
273
|
+
private armWarmWatch;
|
|
274
|
+
/**
|
|
275
|
+
* Re-issue this stream's media start, asking for a REAL start while nothing has arrived.
|
|
276
|
+
*
|
|
277
|
+
* On an own-session camera a re-issue is a keepalive once the session believes the channel is started, and
|
|
278
|
+
* that belief outlives a station which acknowledged a start and then served nothing: every later re-issue is
|
|
279
|
+
* then a keepalive holding a stream that was never started. Nothing arriving since this watch was armed, across
|
|
280
|
+
* more than one re-issue, is this source's own evidence that the channel is not being served — see
|
|
281
|
+
* {@link fruitlessReissues} for why one is not enough and why what the stream delivered earlier is not
|
|
282
|
+
* evidence. Once media arrives the keepalive is what is wanted, and an attached camera re-sends a full start
|
|
283
|
+
* either way.
|
|
284
|
+
*/
|
|
285
|
+
private reissueStart;
|
|
286
|
+
/**
|
|
287
|
+
* Watch a stream this consumer joined rather than warmed, so a dead one cannot pass for a live one.
|
|
288
|
+
*
|
|
289
|
+
* A reused stream hands a joining consumer the retained keyframe at once, which is evidence about the past:
|
|
290
|
+
* it says the stream WAS being served, not that it still is. A caller commits to media on that frame — a
|
|
291
|
+
* process, a negotiated session — so a stream the station has quietly stopped serving strands it with no
|
|
292
|
+
* deadline, because warming is what arms one and a reuse skips warming by definition.
|
|
293
|
+
*
|
|
294
|
+
* The watch is the warm-up's own, deadline and retry alike, and the first frame to arrive AFTER the join
|
|
295
|
+
* clears it, that being the only frame which says the stream is still being served. A stream still serving
|
|
296
|
+
* clears it long before the deadline fires; one that is not fails its consumers exactly as a cold start that
|
|
297
|
+
* never delivered would.
|
|
298
|
+
*
|
|
299
|
+
* The start is re-issued at once and then on the retry's cadence, because a station that stopped serving a
|
|
300
|
+
* channel when its last consumer left is the very case the retry recovers: waiting the whole window to
|
|
301
|
+
* report what one re-issued start can fix is a timeout where a stream was available.
|
|
302
|
+
*
|
|
303
|
+
* Nothing is armed while a watch is already pending, so several consumers joining one reused stream share the
|
|
304
|
+
* watch the first of them started.
|
|
305
|
+
*/
|
|
306
|
+
private watchReusedStream;
|
|
307
|
+
/** Arm the battery budget timer (battery/solar sources) — replaces any pending budget/grace. */
|
|
308
|
+
private armBudget;
|
|
309
|
+
private clearBudget;
|
|
310
|
+
/**
|
|
311
|
+
* Battery budget elapsed: notify consumers (with an {@link StreamBudgetNotice.extend} handle) and arm
|
|
312
|
+
* the grace timer. If no one extends within the grace, auto-stop the pull to protect the battery.
|
|
313
|
+
*/
|
|
314
|
+
private onBudgetExpire;
|
|
315
|
+
/** Re-push the battery budget (host called `extend()` from the notice), cancelling the auto-stop. */
|
|
316
|
+
private extendBudget;
|
|
317
|
+
/** Settle a reuse watch on any frame — the join already holds a decodable picture. */
|
|
318
|
+
private settleReuseWatch;
|
|
319
|
+
/** Stop the warm-up retry + deadline (the stream is confirmed live). */
|
|
320
|
+
private clearWarmWatch;
|
|
321
|
+
/**
|
|
322
|
+
* No keyframe within the warm-up window — surface a start failure to consumers, tear down, and report the
|
|
323
|
+
* failed start to the owner (see {@link SharedLiveSourceOptions.onStartFailed}) so it can recycle what
|
|
324
|
+
* this source cannot reach.
|
|
325
|
+
*/
|
|
326
|
+
private onWarmTimeout;
|
|
327
|
+
/**
|
|
328
|
+
* The typed failure for a start that produced no keyframe, staged by what the source did deliver: nothing
|
|
329
|
+
* at all, audio without a single video frame, or access units a decoder cannot begin at.
|
|
330
|
+
*
|
|
331
|
+
* Video takes precedence when both arrived: audio alongside video says nothing a caller needs, while video
|
|
332
|
+
* without a keyframe does.
|
|
333
|
+
*/
|
|
334
|
+
private startFailure;
|
|
335
|
+
private onVideo;
|
|
336
|
+
/**
|
|
337
|
+
* The coded configuration this frame belongs to: what the parameter sets state, or the frame header's own
|
|
338
|
+
* report where they state nothing readable.
|
|
339
|
+
*
|
|
340
|
+
* The parameter sets are preferred because they define the size a decoder produces while the header only
|
|
341
|
+
* reports it. The fMP4 muxer prefers them for the same reason, though it answers from the sets ONE unit
|
|
342
|
+
* carried rather than from the sets in force, so the two can differ on a keyframe that re-states only a
|
|
343
|
+
* PPS — a muxer is handed frames, not this source's fold.
|
|
344
|
+
*
|
|
345
|
+
* Falling back rather than staying silent is what lets a consumer act on the announcement alone. A set
|
|
346
|
+
* whose geometry cannot be read would otherwise leave it with nothing to rebuild on, which is worse than
|
|
347
|
+
* the header it would have had to diff for itself.
|
|
348
|
+
*
|
|
349
|
+
* Only a keyframe carries parameter sets, so the read costs one parse per announcement: `updatedParamSets`
|
|
350
|
+
* answers with the same object when a frame announces none, and identity settles it from there.
|
|
351
|
+
*/
|
|
352
|
+
private configOf;
|
|
353
|
+
private onAudio;
|
|
354
|
+
/**
|
|
355
|
+
* Retain the rolling window, trimmed to the same run a full-window drain asks for.
|
|
356
|
+
*
|
|
357
|
+
* Retention and drain obey one rule, because a ring trimmed tighter than the drain's rule cannot
|
|
358
|
+
* answer it — the media would already be gone. That rule is {@link windowStart}.
|
|
359
|
+
*
|
|
360
|
+
* A keyframe is the only place a run may begin, so it is also the only anchor a time-based trim has. A
|
|
361
|
+
* stream that stops coding them keeps its last decodable run until another keyframe gives the trim
|
|
362
|
+
* somewhere safe to move to: a separate frame-count ceiling would override the configured window, and
|
|
363
|
+
* cutting mid-group would leave retained media no decoder can start from.
|
|
364
|
+
*/
|
|
365
|
+
private pushRing;
|
|
366
|
+
/**
|
|
367
|
+
* Drain the rolling prebuffer: a decodable run covering the last `seconds` of retained media, capped
|
|
368
|
+
* at the configured `preBufferSeconds`. Asking for none hands over none. The host decides when to
|
|
369
|
+
* drain (e.g. on a motion event) and where to send it.
|
|
370
|
+
*
|
|
371
|
+
* The run opens on the newest keyframe at or before the window starts, so it covers the whole request
|
|
372
|
+
* and over-delivers by however far back that keyframe sits — one keyframe interval on a steady stream,
|
|
373
|
+
* more where delivery stalled, since retention is timed on arrival and a frame carries no device clock.
|
|
374
|
+
* Beginning inside the window instead would under-deliver by that same distance, which on a short window
|
|
375
|
+
* is most of it, and a decoder allows no third option.
|
|
376
|
+
*/
|
|
377
|
+
ringBuffer(seconds: number): LiveVideoFrame[];
|
|
378
|
+
private bufferedMedia;
|
|
379
|
+
/**
|
|
380
|
+
* Where a decodable run covering everything from `cutoff` onwards begins in the ring.
|
|
381
|
+
*
|
|
382
|
+
* The newest keyframe at or before `cutoff` is that place: it is the latest point a decoder can start
|
|
383
|
+
* from and still produce every frame in the window. When the ring reaches no further back than the
|
|
384
|
+
* cutoff, its oldest keyframe is the most of the window that exists. When it holds no keyframe at all,
|
|
385
|
+
* nothing in it is decodable and index `0` reports that to the caller, which checks.
|
|
386
|
+
*/
|
|
387
|
+
private windowStart;
|
|
388
|
+
private isKeyframe;
|
|
389
|
+
/**
|
|
390
|
+
* Handle a consumer leaving. When the last one detaches (1→0), release the station-session user via
|
|
391
|
+
* {@link SharedLiveSourceOptions.onIdle} (its own longer idle timer then arms) and arm the stream's
|
|
392
|
+
* linger teardown.
|
|
393
|
+
*/
|
|
394
|
+
private onDetach;
|
|
395
|
+
/** Refcount hit zero — arm the linger teardown. A new attach in the window cancels it. */
|
|
396
|
+
private arm;
|
|
397
|
+
/**
|
|
398
|
+
* Stop + drop the underlying stream and clear the prime/ring caches. Rebuildable via attach().
|
|
399
|
+
*
|
|
400
|
+
* The stream reference is dropped BEFORE stopping it, because `stop()` emits `"stop"` synchronously and
|
|
401
|
+
* this source listens for that — so stopping re-enters `teardown` through {@link onUpstreamEnd}. Clearing
|
|
402
|
+
* first makes that re-entry hit the `!this.stream` guard and return, which is what keeps a single
|
|
403
|
+
* teardown from reporting a failed start twice (and, before that report existed, from tearing down twice).
|
|
404
|
+
*
|
|
405
|
+
* `report` is false only for {@link dispose}: the owner asked for that one.
|
|
406
|
+
*/
|
|
407
|
+
private teardown;
|
|
408
|
+
/**
|
|
409
|
+
* Underlying stream ended unexpectedly (station max-duration / reconnect): tell consumers.
|
|
410
|
+
*
|
|
411
|
+
* An end before the first keyframe is also a failed start, so those consumers get the typed `error`
|
|
412
|
+
* explaining why nothing played and then the `stop` that closes them — a bare `stop` would look like a
|
|
413
|
+
* normal end of stream to a caller still waiting for its first frame.
|
|
414
|
+
*
|
|
415
|
+
* Teardown runs even if notifying a consumer throws, because what it releases — the upstream stream, the
|
|
416
|
+
* warm-up timers, the ring — belongs to this source and not to the caller whose listener raised. The throw
|
|
417
|
+
* itself still propagates: a listener that raises is the caller's defect to see, not this source's to
|
|
418
|
+
* swallow.
|
|
419
|
+
*/
|
|
420
|
+
private onUpstreamEnd;
|
|
421
|
+
/** Underlying stream failed: tell consumers, then tear down regardless (see {@link onUpstreamEnd}). */
|
|
422
|
+
private onUpstreamError;
|
|
423
|
+
/**
|
|
424
|
+
* Permanent shutdown (session close / router closeAll). Consumers get `stop`; no rebuild.
|
|
425
|
+
*
|
|
426
|
+
* Releases the session user when consumers were still attached: {@link SharedLiveSourceOptions.onActive}
|
|
427
|
+
* fired on the 0→1 transition, and this is the 1→0 one, so skipping it would leave the station pinned
|
|
428
|
+
* open for a source that can never serve anyone again.
|
|
429
|
+
*/
|
|
430
|
+
dispose(): void;
|
|
431
|
+
}
|