@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.
Files changed (143) hide show
  1. package/README.md +31 -12
  2. package/dist/client/device-registry.d.ts +343 -0
  3. package/dist/client/eufy-mega.d.ts +872 -0
  4. package/dist/client/index.d.ts +1 -6
  5. package/dist/client/map-channels.d.ts +22 -0
  6. package/dist/client/types.d.ts +383 -0
  7. package/dist/core/contracts.d.ts +898 -0
  8. package/dist/core/crypto.d.ts +98 -0
  9. package/dist/core/index.d.ts +9 -7
  10. package/dist/core/logger.d.ts +53 -0
  11. package/dist/core/lz4-block.d.ts +35 -0
  12. package/dist/core/raw-dp-hex.d.ts +32 -0
  13. package/dist/core/raw-dp-writer.d.ts +83 -0
  14. package/dist/core/store.d.ts +43 -0
  15. package/dist/core/types.d.ts +169 -0
  16. package/dist/core/util.d.ts +78 -0
  17. package/dist/index.d.ts +4 -7
  18. package/dist/index.js +25526 -14
  19. package/dist/index.js.map +7 -1
  20. package/dist/model/capabilities/access.d.ts +127 -0
  21. package/dist/model/capabilities/arming.d.ts +201 -0
  22. package/dist/model/capabilities/audio.d.ts +154 -0
  23. package/dist/model/capabilities/battery.d.ts +363 -0
  24. package/dist/model/capabilities/camera.d.ts +564 -0
  25. package/dist/model/capabilities/co.d.ts +40 -0
  26. package/dist/model/capabilities/contact.d.ts +133 -0
  27. package/dist/model/capabilities/doorbell.d.ts +345 -0
  28. package/dist/model/capabilities/dp-catalog.d.ts +38 -0
  29. package/dist/model/capabilities/index.d.ts +561 -0
  30. package/dist/model/capabilities/info.d.ts +28 -0
  31. package/dist/model/capabilities/keypad.d.ts +61 -0
  32. package/dist/model/capabilities/leak.d.ts +43 -0
  33. package/dist/model/capabilities/light.d.ts +174 -0
  34. package/dist/model/capabilities/locate.d.ts +63 -0
  35. package/dist/model/capabilities/lock.d.ts +242 -0
  36. package/dist/model/capabilities/manifest.d.ts +107 -0
  37. package/dist/model/capabilities/members.d.ts +647 -0
  38. package/dist/model/capabilities/motion.d.ts +377 -0
  39. package/dist/model/capabilities/person-detection.d.ts +8 -0
  40. package/dist/model/capabilities/ptz.d.ts +289 -0
  41. package/dist/model/capabilities/rtsp.d.ts +221 -0
  42. package/dist/model/capabilities/siren.d.ts +218 -0
  43. package/dist/model/capabilities/smart-light.d.ts +172 -0
  44. package/dist/model/capabilities/smoke.d.ts +40 -0
  45. package/dist/model/capabilities/snapshot.d.ts +6 -0
  46. package/dist/model/capabilities/storage.d.ts +11 -0
  47. package/dist/model/capabilities/suction.d.ts +104 -0
  48. package/dist/model/capabilities/types.d.ts +484 -0
  49. package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
  50. package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
  51. package/dist/model/capabilities/video.d.ts +6 -0
  52. package/dist/model/classify.d.ts +78 -0
  53. package/dist/model/clean-record-detail.d.ts +65 -0
  54. package/dist/model/clean-records.d.ts +69 -0
  55. package/dist/model/device-family.d.ts +73 -0
  56. package/dist/model/device-types.d.ts +123 -0
  57. package/dist/model/device.d.ts +265 -0
  58. package/dist/model/index.d.ts +29 -4
  59. package/dist/model/infer.d.ts +23 -0
  60. package/dist/model/inspect.d.ts +61 -0
  61. package/dist/model/life-params.d.ts +21 -0
  62. package/dist/model/map-pixels.d.ts +70 -0
  63. package/dist/model/param-dictionary.d.ts +28 -0
  64. package/dist/model/param-namespace.d.ts +21 -0
  65. package/dist/model/proto-read.d.ts +53 -0
  66. package/dist/model/push-events.d.ts +147 -0
  67. package/dist/model/registry.d.ts +54 -0
  68. package/dist/model/types.d.ts +301 -0
  69. package/dist/model/vacuum-map-store.d.ts +92 -0
  70. package/dist/model/vacuum-map.d.ts +286 -0
  71. package/dist/model/vacuum-scenes.d.ts +76 -0
  72. package/dist/model/vacuum-schedules.d.ts +85 -0
  73. package/dist/transport/dp-preset.d.ts +102 -0
  74. package/dist/transport/ff09.d.ts +444 -0
  75. package/dist/transport/ffmpeg.d.ts +86 -0
  76. package/dist/transport/http/decodeImageV1.d.ts +20 -0
  77. package/dist/transport/http/decodeImageV2.d.ts +19 -0
  78. package/dist/transport/http/index.d.ts +5 -0
  79. package/dist/transport/http/light-catalog.d.ts +62 -0
  80. package/dist/transport/http/media-download.d.ts +14 -0
  81. package/dist/transport/http/mega-client.d.ts +514 -0
  82. package/dist/transport/http/phone-model.d.ts +21 -0
  83. package/dist/transport/index.d.ts +10 -7
  84. package/dist/transport/mqtt/app-client-id.d.ts +16 -0
  85. package/dist/transport/mqtt/availability.d.ts +14 -0
  86. package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
  87. package/dist/transport/mqtt/biz-stream.d.ts +98 -0
  88. package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
  89. package/dist/transport/mqtt/clean-codec.d.ts +14 -0
  90. package/dist/transport/mqtt/command-router.d.ts +285 -0
  91. package/dist/transport/mqtt/dp-codec.d.ts +58 -0
  92. package/dist/transport/mqtt/dp-color.d.ts +14 -0
  93. package/dist/transport/mqtt/engine.d.ts +16 -0
  94. package/dist/transport/mqtt/index.d.ts +5 -0
  95. package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
  96. package/dist/transport/mqtt/topics.d.ts +80 -0
  97. package/dist/transport/p2p/adts.d.ts +91 -0
  98. package/dist/transport/p2p/annexb.d.ts +124 -0
  99. package/dist/transport/p2p/codec.d.ts +166 -0
  100. package/dist/transport/p2p/command-router.d.ts +655 -0
  101. package/dist/transport/p2p/commands.d.ts +550 -0
  102. package/dist/transport/p2p/envelope.d.ts +46 -0
  103. package/dist/transport/p2p/fmp4.d.ts +89 -0
  104. package/dist/transport/p2p/fragment-recording.d.ts +33 -0
  105. package/dist/transport/p2p/index.d.ts +13 -0
  106. package/dist/transport/p2p/lan-ip.d.ts +25 -0
  107. package/dist/transport/p2p/live-stream.d.ts +223 -0
  108. package/dist/transport/p2p/live-trace.d.ts +123 -0
  109. package/dist/transport/p2p/media.d.ts +105 -0
  110. package/dist/transport/p2p/p2p-session.d.ts +620 -0
  111. package/dist/transport/p2p/readable-egress.d.ts +27 -0
  112. package/dist/transport/p2p/session-manager.d.ts +154 -0
  113. package/dist/transport/p2p/shared-live-source.d.ts +431 -0
  114. package/dist/transport/p2p/talkback.d.ts +187 -0
  115. package/dist/transport/p2p/video.d.ts +150 -0
  116. package/dist/transport/p2p/write-commands.d.ts +21 -0
  117. package/dist/transport/protobuf.d.ts +5 -0
  118. package/dist/transport/push/fcm.d.ts +23 -0
  119. package/dist/transport/push/index.d.ts +6 -0
  120. package/dist/transport/push/message-tags.d.ts +26 -0
  121. package/dist/transport/push/parser.d.ts +27 -0
  122. package/dist/transport/push/proto.d.ts +11 -0
  123. package/dist/transport/push/push-client.d.ts +64 -0
  124. package/dist/transport/push/store.d.ts +23 -0
  125. package/dist/transport/push/types.d.ts +180 -0
  126. package/dist/transport/raw-dp.d.ts +6 -0
  127. package/dist/transport/stored-image-cache.d.ts +23 -0
  128. package/dist/transport/tuya/account.d.ts +44 -0
  129. package/dist/transport/tuya/client.d.ts +89 -0
  130. package/dist/transport/tuya/command-router.d.ts +79 -0
  131. package/dist/transport/tuya/dp-codec.d.ts +56 -0
  132. package/dist/transport/tuya/index.d.ts +28 -0
  133. package/dist/transport/tuya/request.d.ts +157 -0
  134. package/dist/transport/tuya/sign.d.ts +64 -0
  135. package/package.json +12 -13
  136. package/dist/client/index.js +0 -2
  137. package/dist/client/index.js.map +0 -1
  138. package/dist/core/index.js +0 -2
  139. package/dist/core/index.js.map +0 -1
  140. package/dist/model/index.js +0 -2
  141. package/dist/model/index.js.map +0 -1
  142. package/dist/transport/index.js +0 -2
  143. package/dist/transport/index.js.map +0 -1
@@ -0,0 +1,33 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { FragmentRecordingHandle, MediaFragment, StreamBudgetNotice } from "../../core/contracts.js";
3
+ import type { SharedLiveSource } from "./shared-live-source.js";
4
+ export interface FragmentRecordingOptions {
5
+ fragmentSeconds?: number;
6
+ preBufferSeconds?: number;
7
+ }
8
+ /**
9
+ * One caller-owned fragmented recording over a shared live source. Buffered and live frames pass
10
+ * through the same timestamp-aware muxer, while budget notices retain the source's `extend()` handle.
11
+ */
12
+ export declare class FragmentRecording extends EventEmitter implements FragmentRecordingHandle {
13
+ private readonly opts;
14
+ private readonly mux;
15
+ private readonly queue;
16
+ private consumer?;
17
+ private wake?;
18
+ private failure?;
19
+ private ended;
20
+ private iterated;
21
+ private held;
22
+ private readonly ready;
23
+ constructor(source: Promise<SharedLiveSource>, opts?: FragmentRecordingOptions);
24
+ on(event: "budget", listener: (notice: StreamBudgetNotice) => void): this;
25
+ stop(): void;
26
+ [Symbol.asyncIterator](): AsyncIterator<MediaFragment>;
27
+ private attach;
28
+ private ingest;
29
+ /** Hand over the next fragment, releasing the consumer once the owner is back inside the bound. */
30
+ private take;
31
+ private fail;
32
+ private nudge;
33
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./p2p-session.js";
2
+ export * from "./live-stream.js";
3
+ export * from "./video.js";
4
+ export * from "./media.js";
5
+ export { SharedLiveSource, type SharedLiveState, type SharedLiveSourceOptions, type TimedMediaFrame, type Consumer as LiveConsumer, } from "./shared-live-source.js";
6
+ export { extractParamSets, codedGeometry, type ParamSets, type CodedGeometry, type Size } from "./annexb.js";
7
+ export { type PowerTier } from "./session-manager.js";
8
+ export * from "./commands.js";
9
+ export * from "./envelope.js";
10
+ export * from "./write-commands.js";
11
+ export * from "./lan-ip.js";
12
+ export { LIVE_TRACE_MESSAGE, type LiveTrace } from "./live-trace.js";
13
+ export * as p2pCodec from "./codec.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * LAN-address resolution for a P2P station from its cloud device record.
3
+ *
4
+ * Pure helpers (no session state) — used when opening a P2P session to prefer a direct on-LAN
5
+ * lookup over the cloud relay (works when broadcast is blocked by AP isolation / macOS, even if
6
+ * the record's pairing `ip_addr` went stale).
7
+ */
8
+ /** True for an RFC-1918 private IPv4 (a routable LAN address, not a WAN/public one). */
9
+ export declare function isPrivateIpv4(s: string): boolean;
10
+ /**
11
+ * Resolve the freshest LAN IP for a station from its cloud record.
12
+ *
13
+ * The record carries the device IP in several places of differing trust:
14
+ * - **params** (`{param_type, param_value, update_time}`) — self-reported by the device on each
15
+ * heartbeat, so a private-IP param value is timestamped and updates when the device roams.
16
+ * - **`ip_addr`** (top-level) — frozen at pairing, no timestamp; goes stale if the device moved
17
+ * to another subnet since (observed live: a SoloCam still advertising a 192.168.86.x pairing
18
+ * address while actually on 192.168.1.x).
19
+ * - **`local_ip`** — often empty, or the public WAN address.
20
+ *
21
+ * Strategy: collect every param whose value is a private IPv4, take the one with the newest
22
+ * `update_time`, and only fall back to `ip_addr` / `local_ip` when no param IP exists. This
23
+ * prefers the most recent evidence over a fixed field-priority order.
24
+ */
25
+ export declare function freshestLanIp(raw: unknown): string | undefined;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Managed realtime live stream over P2P — turns a station's `CMD_VIDEO_FRAME` (1300) /
3
+ * `CMD_AUDIO_FRAME` (1301) frames into a clean, continuous **Annex-B H.264** (+ audio) elementary
4
+ * feed a muxer can ingest.
5
+ *
6
+ * Two video frame variants are handled:
7
+ * - **Plaintext** (`signCode 0`, e.g. HomeBase-attached cameras): a fixed 22-byte frame header
8
+ * (`parseVideoFrameHeader`) followed directly by Annex-B NAL units — verified live (960×540,
9
+ * keyframe = header flag bit0). We strip the 22-byte header.
10
+ * - **Encrypted** (E2E cameras): the body is AES-256-GCM under a per-stream media key wrapped in a
11
+ * keyframe ECIES envelope. If an `eccPrivateKey` is supplied we run `VideoFrameDecoder`; otherwise
12
+ * those frames are skipped (no key → no video).
13
+ *
14
+ * Emits: `video` ({@link LiveVideoFrame} — Annex-B), `audio` ({@link LiveAudioFrame}), `start`, `stop`,
15
+ * `error`.
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ import { type P2PSession } from "./p2p-session.js";
19
+ import { type Logger } from "../../core/logger.js";
20
+ import type { LiveAudioFrame, LiveVideoFrame } from "../../core/contracts.js";
21
+ /**
22
+ * How often the media start is re-issued to hold a stream open, when a caller expresses no preference.
23
+ *
24
+ * This is **on by default** because some cameras stop sending without it. With the nudge disabled, a
25
+ * T8170 went quiet 13.6 s into a plain `live()` — no `stop`, no `error`, the feed simply stopped — and
26
+ * ran the full window with it on (185 frames vs 704 over 40 s).
27
+ *
28
+ * Which cameras need it does **not** follow topology or power source, so there is no predicate to gate
29
+ * it on: measured across four cameras, an own-session T8410 and both HomeBase-attached cameras (T8210,
30
+ * T8114) held a 40 s stream up with the nudge disabled, while the own-session T8170 did not. All four
31
+ * are battery. Since a camera that needs it goes silent rather than reporting anything, the default
32
+ * covers the one that fails.
33
+ *
34
+ * What the nudge costs differs by topology, and only one branch is a true keepalive: an own-session
35
+ * camera's `startLiveMedia` tracks that the stream is already started and sends the small ping, while a
36
+ * HomeBase-attached camera has no such state and re-sends the full media start — a genuine restart on
37
+ * that path.
38
+ *
39
+ * That restart is harmless on a station serving ONE camera and harmful on a station serving several: it
40
+ * re-asserts this camera's channel every interval, so two attached streams contend continuously. Measured on
41
+ * a real base as a full start every 3 s from each. So an attached stream stops nudging as soon as the station
42
+ * delivers a frame of its own channel — see the private `settleKeepalive`. The measurement that justifies it
43
+ * is the one above: the attached cameras held their window with the nudge disabled outright.
44
+ */
45
+ export declare const DEFAULT_KEEPALIVE_MS = 3000;
46
+ export interface LiveStreamOptions {
47
+ /**
48
+ * Camera channel to START (the device's `device_channel`) — sent in CMD_START_REALTIME_MEDIA to
49
+ * select which camera on a multi-camera HomeBase streams. Defaults to the station channel.
50
+ *
51
+ * On a HomeBase-attached camera it is ALSO what inbound media is matched against, so one camera's stream
52
+ * never carries another camera's frames: a station fanning several cameras out over one session tags every
53
+ * media frame with the camera it belongs to. A frame tagged for a channel a sibling started is never taken,
54
+ * however long the station keeps serving that sibling instead of this one.
55
+ */
56
+ channel?: number;
57
+ /** Camera ECC private key (32B) for E2E/encrypted cameras; omit for plaintext cameras. */
58
+ eccPrivateKey?: Buffer;
59
+ /** Admin account id — required for the level-2 (`signCode 8`) media-start payload selecting a camera. */
60
+ accountId?: string;
61
+ /**
62
+ * Re-send the media start every N ms to hold the stream open. Defaults to
63
+ * {@link DEFAULT_KEEPALIVE_MS}; pass `0` to disable.
64
+ */
65
+ keepAliveMs?: number;
66
+ /**
67
+ * How long an attached stream tolerates silence on its own channel before re-asserting again, in ms.
68
+ *
69
+ * The re-assert is settled by the first own-channel frame, because settling it is what stops two attached
70
+ * streams contending on a station that serves one camera at a time. Silence for this long says the station
71
+ * is no longer serving this camera, which is the only condition the re-assert was for. Defaults to twice
72
+ * the keepalive interval, so a stream whose media flows never reaches it.
73
+ */
74
+ stallMs?: number;
75
+ /**
76
+ * Whether re-asserting this camera's channel is still wanted, consulted each time the stall window
77
+ * elapses. Absent means always wanted.
78
+ *
79
+ * A re-assert on an attached camera is a full media start, so it takes the station from whichever camera
80
+ * it was serving. Whether that is wanted depends on who is attached to this pull and to its siblings,
81
+ * which this stream cannot see. Its owner can, so it asks rather than assuming.
82
+ *
83
+ * The watch keeps re-arming while this answers false, so a pull that gains a consumer re-asserts at the
84
+ * next window rather than staying silent for the rest of its life.
85
+ */
86
+ reassertWanted?: () => boolean;
87
+ /**
88
+ * Runtime topology fact (from the device record: `parent_sn && parent_sn !== sn`): true = the camera
89
+ * rides a HomeBase's session (start via the level-2 `1003` payload), false = own-session camera
90
+ * (start via the `1700`/`cmd 1000` path, level-2 or level-1 per the session key). NOT a family trait.
91
+ */
92
+ homeBaseAttached?: boolean;
93
+ /** Diagnostics sink. Omit for silence. */
94
+ logger?: Logger;
95
+ }
96
+ export declare class LiveStream extends EventEmitter {
97
+ private readonly session;
98
+ private readonly opts;
99
+ private listening;
100
+ private decoder?;
101
+ private kaTimer?;
102
+ /** Last codec sniffed off a keyframe; delta frames (no config NAL) inherit it. Default h264. */
103
+ private lastCodec;
104
+ /** Rebuilds an access unit the station split across several frames — see {@link AccessUnitAssembler}. */
105
+ private readonly units;
106
+ /** The channel inbound media must be tagged with, once {@link acceptsMedia} trusts the station's tag. */
107
+ private mediaChannel?;
108
+ private tracedFirstVideoCommand;
109
+ private tracedFirstVideoUnit;
110
+ private tracedFirstKeyframe;
111
+ private tracedDecodeFailures;
112
+ private tracedFirstForeignFrame;
113
+ private stallTimer?;
114
+ /**
115
+ * The channel this stream starts, stops, traces under and matches its own abandonment on. An omitted
116
+ * channel resolves to {@link STATION_CHANNEL} — the value the session resolves it to.
117
+ */
118
+ private readonly channel;
119
+ private readonly handler;
120
+ /** Forwards `liveStartUnacknowledged` only where it carries this stream's own channel. */
121
+ private readonly unackedHandler;
122
+ private readonly logger;
123
+ constructor(session: P2PSession, opts?: LiveStreamOptions);
124
+ /** Emit a live trace under its session's handle and the channel this stream pulls. */
125
+ private trace;
126
+ /** Begin streaming: attach the frame listener and tell the station to start realtime media. */
127
+ start(): this;
128
+ /**
129
+ * Stop re-issuing the media start once this camera's own media has arrived, on an attached camera.
130
+ *
131
+ * The nudge differs by topology and only one branch is a ping: an own-session camera sends a small
132
+ * keepalive, while an attached camera has no such state and re-sends the FULL media start. On a station that
133
+ * serves one camera at a time that restart re-asserts this channel against whatever else is warm, so two
134
+ * attached streams restart every interval and contend for the station continuously — measured on a real base
135
+ * as a full start every 3 s from each.
136
+ *
137
+ * A frame of this camera's own channel is the station stating it is serving THIS camera, which is the only
138
+ * thing the restart was trying to bring about. The SDK's own measurement agrees it is then unnecessary: both
139
+ * attached cameras held a 40 s stream with the nudge disabled, while the own-session camera that needs it
140
+ * went quiet at 13.6 s without it — so an own-session stream keeps its ping.
141
+ *
142
+ * The warm-up retry a shared source runs is untouched: it recovers a start that raced the level-2 key, and
143
+ * it stops at the first keyframe by its own rule.
144
+ */
145
+ private settleKeepalive;
146
+ /**
147
+ * Re-arm the attached re-assert if this camera's own media goes silent.
148
+ *
149
+ * Holding the re-assert off is right while the station is serving this camera and wrong the moment it stops:
150
+ * a station that switches to a sibling leaves this stream with no frames, no error and no `stop`, and the
151
+ * warm-up watch that would have caught it was cleared by the first frame. Silence is therefore the condition
152
+ * the re-assert exists for, and the only one — re-arming while media flows is the contention this settle was
153
+ * introduced to remove.
154
+ *
155
+ * Replaced on every own-channel frame, so the window is measured from the last one.
156
+ *
157
+ * Gated on {@link LiveStreamOptions.reassertWanted}: silence with nobody attached is not a condition to
158
+ * act on, because the re-assert would take the station from a camera someone is watching.
159
+ */
160
+ private armStallWatch;
161
+ /**
162
+ * Re-issue the start command (idempotent while listening) — the media-start / keepalive nudge. The
163
+ * shared source calls this to retry a start that raced key negotiation, until frames flow. Safe to
164
+ * call repeatedly: `startLiveMedia` self-selects start vs keepalive per the session state.
165
+ */
166
+ nudge(force?: boolean): void;
167
+ private sendStart;
168
+ /** Stop streaming: detach the listener and tell the station to stop. Idempotent. */
169
+ stop(): void;
170
+ /** Extract the Annex-B payload from a plaintext 1300 frame (fixed 22-byte header), or undefined. */
171
+ private plaintextAnnexB;
172
+ private onFrame;
173
+ /**
174
+ * Whether this stream may take `frame` — the demultiplexer for a station that serves several cameras.
175
+ *
176
+ * Every stream over a station's session reads the same inbound feed, so with two cameras warm each stream
177
+ * sees both. The station DOES say which camera a media frame belongs to: measured with two cameras of
178
+ * different geometry streaming at once on one HomeBase, the frame's channel field partitioned them
179
+ * exactly — 43 frames of 1920x1080 on the started channel 0 and 28 of 640x480 on channel 2, video and
180
+ * audio alike — while each handle was delivered both cameras' frames.
181
+ *
182
+ * The tag is only meaningful where a station fans out to several cameras. A camera that owns its session
183
+ * numbers its stream for itself: one was started on channel 0 and tagged its frames channel 1, so
184
+ * matching there would drop the whole stream. Hence only an attached camera filters.
185
+ *
186
+ * The match is UNCONDITIONAL, however long a station serves another camera instead of this one.
187
+ *
188
+ * A station serving one camera at a time hands a newly opened camera nothing but its sibling's frames until
189
+ * it switches, so "no media of my own yet, plenty for someone else" is what an ordinary handover looks like
190
+ * and does not distinguish a station that tags differently from one that is simply busy.
191
+ *
192
+ * A stream receiving none of its own media reaches the warm-up deadline and raises a typed start failure
193
+ * naming the stage it got to. Delivering another camera's picture raises nothing.
194
+ */
195
+ private acceptsMedia;
196
+ /**
197
+ * The Annex-B payload of a frame that starts an access unit. The session decodes it when it can (it
198
+ * holds the RSA private key, and handles the plaintext form); the legacy header-strip and the ECIES
199
+ * {@link VideoFrameDecoder} are the fallbacks for an E2E camera.
200
+ */
201
+ private annexbOf;
202
+ /**
203
+ * Report an access unit the transport could not complete.
204
+ *
205
+ * Loud once per stream, then quiet: a station losing datagrams steadily would otherwise flood a host's
206
+ * log with one line per unit, and the first one already says everything the rest repeat. A dropped unit is
207
+ * otherwise silent in both directions: no frame reaches a consumer, and nothing states why.
208
+ */
209
+ private reportDroppedUnit;
210
+ }
211
+ export interface LiveStream {
212
+ on(event: "video", listener: (frame: LiveVideoFrame) => void): this;
213
+ on(event: "audio", listener: (frame: LiveAudioFrame) => void): this;
214
+ on(event: "start" | "stop", listener: () => void): this;
215
+ on(event: "error", listener: (err: Error) => void): this;
216
+ on(event: "budget", listener: (notice: import("../../core/contracts.js").StreamBudgetNotice) => void): this;
217
+ /** This channel's media start was abandoned unacknowledged — see {@link LiveStreamHandle}. */
218
+ on(event: "unacknowledged", listener: () => void): this;
219
+ emit(event: "video", frame: LiveVideoFrame): boolean;
220
+ emit(event: "audio", frame: LiveAudioFrame): boolean;
221
+ emit(event: "start" | "stop" | "unacknowledged"): boolean;
222
+ emit(event: "error", err: Error): boolean;
223
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Bounded, identity-free live-startup diagnostics.
3
+ *
4
+ * A live start spans two modules — the session issues the media command and reassembles datagrams, the
5
+ * stream turns accepted frames into access units — so the message and its phase vocabulary have one owner
6
+ * here rather than a literal repeated at each call site. A caller reads these to tell one startup outcome
7
+ * from another, and matches on {@link LIVE_TRACE_MESSAGE} plus a {@link LiveTrace} phase to do it.
8
+ *
9
+ * Every field is a fixed label, a boolean, a data-type id, or a sign code. No serial, P2P identifier,
10
+ * address, account id, key material, or media byte is carried, so the records are safe in a host's log.
11
+ *
12
+ * @module p2p/live-trace
13
+ */
14
+ import type { Logger } from "../../core/logger.js";
15
+ /** The message every startup trace is logged under. */
16
+ export declare const LIVE_TRACE_MESSAGE = "[live] start trace";
17
+ /** One bounded startup observation. */
18
+ export type LiveTrace =
19
+ /** A media start or keepalive was sent, with the topology and encryption level it was sent under. */
20
+ {
21
+ phase: "media-command";
22
+ topology: "attached" | "own";
23
+ action: "start" | "keepalive";
24
+ level2: boolean;
25
+ }
26
+ /** The device acknowledged a retained start, or that start was repeated / abandoned unacknowledged. */
27
+ | {
28
+ phase: "media-command-ack" | "media-command-retry" | "media-command-unacknowledged";
29
+ action: "start";
30
+ }
31
+ /** The first inbound video command, and whether this stream's channel filter accepted it. */
32
+ | {
33
+ phase: "first-video-command";
34
+ signCode: number;
35
+ accepted: boolean;
36
+ }
37
+ /** The first reassembled video access unit, and whether it was decodable on its own. */
38
+ | {
39
+ phase: "first-video-unit";
40
+ keyframe: boolean;
41
+ }
42
+ /** The first keyframe reached the consumer. */
43
+ | {
44
+ phase: "first-keyframe";
45
+ }
46
+ /** Media tagged for another camera on the same station arrived first. */
47
+ | {
48
+ phase: "first-foreign-media-command";
49
+ media: "audio" | "video";
50
+ }
51
+ /** A video payload decoded to nothing, so no access unit could be built from it. */
52
+ | {
53
+ phase: "video-decode-empty";
54
+ signCode: number;
55
+ }
56
+ /** A datagram was missing on a data channel, discarding the logical frame being reassembled. */
57
+ | {
58
+ phase: "datagram-gap";
59
+ dataType: number;
60
+ }
61
+ /** A data channel's numbering restarted mid-connection, so sequencing resynchronized onto it. */
62
+ | {
63
+ phase: "sequence-restart";
64
+ dataType: number;
65
+ }
66
+ /**
67
+ * A live start is holding for the station's level-2 key, with the milliseconds it will wait.
68
+ *
69
+ * The first of three phases that account for the wait before any media command is sent. A start that looks
70
+ * slow is either waiting here, waiting for the station to serve the channel it was asked for, or being
71
+ * re-issued — and only these separate them.
72
+ */
73
+ | {
74
+ phase: "level2-wait";
75
+ waitMs: number;
76
+ }
77
+ /** The station's level-2 key was negotiated, under the cipher it selected. */
78
+ | {
79
+ phase: "level2-ready";
80
+ cipherId: number;
81
+ }
82
+ /** The level-2 key did not arrive in its grace, so the start proceeds at level 1 or not at all. */
83
+ | {
84
+ phase: "level2-absent";
85
+ waitedMs: number;
86
+ }
87
+ /** A shared source began warming, with the interval it re-issues on and the deadline it fails at. */
88
+ | {
89
+ phase: "warming";
90
+ retryMs: number;
91
+ deadlineMs: number;
92
+ }
93
+ /**
94
+ * A media command was never put on the wire, and what it was missing.
95
+ *
96
+ * The attached media start has no level-1 form, so without the station's level-2 key there is nothing to
97
+ * send. A command that was never sent is otherwise indistinguishable from one the station ignored, which is
98
+ * the difference between a key that never arrived and a station that is not answering.
99
+ */
100
+ | {
101
+ phase: "media-command-unsent";
102
+ reason: "level2-key" | "address";
103
+ }
104
+ /**
105
+ * This connection's path has stopped answering the heartbeat, with how long it has been silent.
106
+ *
107
+ * The station answers every PING with a PONG, so silence past several heartbeats is the path being gone.
108
+ * Stated only where a pong arrived: a station that has never answered says nothing by not answering now.
109
+ */
110
+ | {
111
+ phase: "path-stale";
112
+ silentMs: number;
113
+ };
114
+ /**
115
+ * Record one startup observation at debug level.
116
+ *
117
+ * `source` states which pull the record belongs to, as an OPAQUE per-process handle — never a serial, a
118
+ * channel or an address. A phase says what happened and nothing about where, so four cameras warming off one
119
+ * HomeBase produce four indistinguishable records; the handle groups them without naming anything, cannot be
120
+ * resolved to a device by whoever reads it, and means nothing in the next run. That is what keeps these
121
+ * records retainable.
122
+ */
123
+ export declare function traceLiveStart(logger: Logger, trace: LiveTrace, source?: string): void;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Camera **media** operations over P2P — live snapshot, live stream, clip recording.
3
+ *
4
+ * These take an already-resolved {@link P2PSession} (the client owns session/channel resolution) and
5
+ * return data. They speak only P2P + ffmpeg — no dependency on the client class — so the client stays
6
+ * thin and this stays the single home for the media protocol. Surfaced to consumers via
7
+ * `device.camera()`.
8
+ *
9
+ * `snapshotLive` / `record` shell out to ffmpeg. The binary is whatever {@link spawnFfmpeg} resolves —
10
+ * the bare name on `PATH` by default, or the executable the caller named (`ffmpegPath`).
11
+ *
12
+ * @module p2p/media
13
+ */
14
+ import { P2PSession } from "./p2p-session.js";
15
+ import { LiveStream, type LiveStreamOptions } from "./live-stream.js";
16
+ import { type FfmpegLevel } from "../ffmpeg.js";
17
+ import { type Logger } from "../../core/logger.js";
18
+ import type { SharedLiveSource } from "./shared-live-source.js";
19
+ /**
20
+ * Open a managed **live stream** on an already-connected session. Returns the {@link LiveStream}
21
+ * already `start()`ed; call `.stop()` when done. (Session/channel/level-2-key resolution is the
22
+ * caller's job — see `EufyMega.resolveSession`.)
23
+ */
24
+ export declare function openLiveStream(session: P2PSession, opts?: LiveStreamOptions): Promise<LiveStream>;
25
+ /**
26
+ * **Live snapshot off a SHARED source** (V6) — snapshot as just another consumer of the shared live
27
+ * pull. If the source is already warm and has a cached keyframe (V2 keyframe-prime), the joining
28
+ * consumer receives that IDR immediately and we decode it with **no extra pull** — a snapshot while
29
+ * someone else watches costs nothing on the wire. Otherwise we warm the source and wait for a clean
30
+ * keyframe (the first IDR after a cold start is frequently partial, so skip it by default). Requires
31
+ * `ffmpeg` for the Annex-B → JPEG decode.
32
+ *
33
+ * The returned dimensions are the ENCODED IMAGE's own, read back out of it — never the frame header's.
34
+ * The header states the stream's geometry when the capture started, and a stream that reconfigures
35
+ * mid-burst leaves it describing something the returned bytes contradict; the return value describes an
36
+ * image, so the image is its source of truth.
37
+ */
38
+ export declare function captureSnapshotFromShared(source: SharedLiveSource, opts?: {
39
+ timeoutMs?: number;
40
+ collectMs?: number;
41
+ skipKeyframes?: number;
42
+ logger?: Logger;
43
+ ffmpegLevel?: FfmpegLevel;
44
+ ffmpegPath?: string;
45
+ }): Promise<{
46
+ jpeg: Buffer;
47
+ width: number;
48
+ height: number;
49
+ }>;
50
+ /**
51
+ * **Record** a clip — collect the live H.264/H.265 stream for `seconds` and mux it to a fragmented
52
+ * MP4 (same source as {@link captureSnapshotFromShared}, kept running and written to a container).
53
+ * Recording starts at the first complete keyframe so the clip is seekable. Requires `ffmpeg`.
54
+ *
55
+ * Opens its OWN {@link LiveStream} over the session rather than joining the device's shared source, so it
56
+ * costs a second pull on a camera already streaming, and the shared path's release of a sibling's lingering
57
+ * pull does not reach it. `recordFragments` is the shared-consumer path.
58
+ *
59
+ * The clip therefore starts at the SECOND keyframe, so parameter sets announced only with the first are
60
+ * dropped along with it — every frame is watched for an announcement, including the skipped ones, and
61
+ * the collected run is primed before muxing. This also settles the codec, which is sniffed from a config NAL:
62
+ * a run of bare slices would otherwise fall back to H.264 and mislabel an H.265 clip.
63
+ *
64
+ * **Bounded in both phases, so it always settles.** The first phase is bounded by `timeoutMs` waiting for the
65
+ * keyframe the clip starts at; the second is bounded by the clip's own window, which is armed as a deadline
66
+ * the moment capture starts rather than being read off the next frame to arrive. A camera that goes quiet
67
+ * mid-clip delivers no further frame to compare a clock against — measured on an own-session camera that
68
+ * stopped 13.6 s into a stream with no `stop` and no `error` — so a clip whose end is decided inside a frame
69
+ * handler has no end at all, and the promise stays pending for the life of the process. The deadline answers
70
+ * with the run collected up to it: the window the caller asked for has elapsed, and frames the camera never
71
+ * sent cannot be waited into existence.
72
+ *
73
+ * A pull whose SESSION goes away before that window elapses fails the clip instead, naming the close. No
74
+ * further frame can arrive on it, so there is nothing left to wait for, and a caller that asked for a clip of
75
+ * a stated length is told the session went away rather than handed a fragment as if it were the clip. A decode
76
+ * failure on the stream fails it the same way.
77
+ *
78
+ * The `error` listener outlives the collection deliberately. An unhandled `error` on an emitter takes the host
79
+ * process down, and the stream is stopped only after the promise settles, so it stays attached and a late
80
+ * failure lands on an already-settled promise as the no-op it is.
81
+ */
82
+ export declare function recordClip(session: P2PSession, seconds: number, opts?: {
83
+ timeoutMs?: number;
84
+ skipKeyframes?: number;
85
+ logger?: Logger;
86
+ ffmpegLevel?: FfmpegLevel;
87
+ ffmpegPath?: string;
88
+ } & LiveStreamOptions): Promise<Buffer>;
89
+ /**
90
+ * The geometry a JPEG declares in its own frame header (`SOFn`), or `undefined` when it carries none.
91
+ *
92
+ * Walks the marker segments from the SOI rather than searching for the marker bytes: a `0xffc0` pair
93
+ * occurs inside quantization tables and entropy-coded data, and the first one found there would answer
94
+ * with two bytes of image content. Every `SOFn` puts precision, then height, then width at the same
95
+ * offset past its length field, so one read serves all of them. Any number of `0xff` fill bytes may
96
+ * precede a marker, and the standalone markers carry no length to skip by — both are what a naive walk
97
+ * gets wrong, and either would make a perfectly good image read as having no geometry.
98
+ *
99
+ * Decoding the image (the `jpeg-js` path the v2 thumbnail decoder needs) would answer the same question,
100
+ * but it is synchronous pure JS over every pixel: this needs a dozen bytes of header, so it reads them.
101
+ */
102
+ export declare function jpegGeometry(jpeg: Buffer): {
103
+ width: number;
104
+ height: number;
105
+ } | undefined;