@mega-yfue/eufy-sdk 0.5.0-beta.5 → 0.5.0-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/client/device-registry.d.ts +24 -2
  2. package/dist/client/eufy-mega.d.ts +57 -6
  3. package/dist/client/types.d.ts +1 -1
  4. package/dist/core/contracts.d.ts +52 -26
  5. package/dist/core/store.d.ts +2 -0
  6. package/dist/core/types.d.ts +2 -0
  7. package/dist/index.js +2489 -286
  8. package/dist/index.js.map +4 -4
  9. package/dist/model/capabilities/arming.d.ts +31 -4
  10. package/dist/model/capabilities/camera.d.ts +24 -10
  11. package/dist/model/capabilities/dp-catalog.d.ts +4 -3
  12. package/dist/model/capabilities/index.d.ts +7 -2
  13. package/dist/model/capabilities/light.d.ts +3 -0
  14. package/dist/model/capabilities/motion.d.ts +5 -0
  15. package/dist/model/capabilities/suction.d.ts +5 -5
  16. package/dist/model/capabilities/types.d.ts +6 -0
  17. package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
  18. package/dist/model/classify.d.ts +1 -2
  19. package/dist/model/device-family.d.ts +4 -0
  20. package/dist/model/registry.d.ts +3 -2
  21. package/dist/model/types.d.ts +11 -0
  22. package/dist/transport/abortable.d.ts +8 -0
  23. package/dist/transport/http/mega-client.d.ts +26 -0
  24. package/dist/transport/mqtt/broker-discovery.d.ts +2 -1
  25. package/dist/transport/mqtt/command-router.d.ts +3 -2
  26. package/dist/transport/p2p/command-router.d.ts +14 -26
  27. package/dist/transport/p2p/fmp4.d.ts +12 -2
  28. package/dist/transport/p2p/live-stream.d.ts +3 -0
  29. package/dist/transport/p2p/p2p-session.d.ts +23 -3
  30. package/dist/transport/p2p/recording-download.d.ts +4 -3
  31. package/dist/transport/p2p/shared-live-source.d.ts +9 -0
  32. package/dist/transport/p2p/talkback.d.ts +21 -10
  33. package/dist/transport/p2p/video.d.ts +5 -2
  34. package/dist/transport/rtc/command-router.d.ts +128 -0
  35. package/dist/transport/rtc/live.d.ts +69 -0
  36. package/dist/transport/rtc/peer.d.ts +104 -0
  37. package/dist/transport/rtc/portal-keepalive.d.ts +11 -0
  38. package/dist/transport/rtc/portal-packet.d.ts +72 -0
  39. package/dist/transport/rtc/ptcs-framer.d.ts +86 -0
  40. package/dist/transport/rtc/scall-sdp.d.ts +42 -0
  41. package/dist/transport/rtc/session.d.ts +90 -0
  42. package/dist/transport/rtc/signaling.d.ts +139 -0
  43. package/dist/transport/token-echo.d.ts +8 -0
  44. package/package.json +2 -1
@@ -277,8 +277,8 @@ export declare class P2PSession extends EventEmitter {
277
277
  private decryptLevel2;
278
278
  get isConnected(): boolean;
279
279
  /**
280
- * Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
281
- * less or refuses.
280
+ * Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and log once per session when it grants
281
+ * less (debug) or refuses (warn).
282
282
  *
283
283
  * The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
284
284
  * inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
@@ -435,6 +435,11 @@ export declare class P2PSession extends EventEmitter {
435
435
  * "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
436
436
  */
437
437
  sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
438
+ /**
439
+ * The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
440
+ * level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
441
+ */
442
+ sendRawLevel1Bytes(payload: Buffer, channel: number, outerCmd: number): boolean;
438
443
  /**
439
444
  * Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
440
445
  * its runtime topology takes, as {@link startLiveMedia} selects its start:
@@ -606,6 +611,18 @@ export declare class P2PSession extends EventEmitter {
606
611
  accountId?: string;
607
612
  channel?: number;
608
613
  }): void;
614
+ /**
615
+ * Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
616
+ * recording rows with companion tables; each group retains its `table_name` and `payload`.
617
+ * `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
618
+ */
619
+ queryRecordPage(opts: {
620
+ accountId?: string;
621
+ startDate: string;
622
+ endDate: string;
623
+ startTime?: string;
624
+ count?: number;
625
+ }): Promise<unknown[]>;
609
626
  /**
610
627
  * Query one on-station table and answer its rows, once the reply is whole.
611
628
  *
@@ -613,13 +630,15 @@ export declare class P2PSession extends EventEmitter {
613
630
  * several frames whose decrypted text is a fragment of one document, so the fragments are
614
631
  * accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
615
632
  * `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
616
- * complete reply.
633
+ * complete reply, and when the station answers a non-zero `mIntRet`.
617
634
  *
618
635
  * One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
619
636
  * session throws, so a second reply cannot land in this buffer.
620
637
  */
621
638
  readDatabase(table: string, opts?: {
622
639
  accountId?: string;
640
+ innerCmd?: number;
641
+ query?: Record<string, unknown>;
623
642
  timeoutMs?: number;
624
643
  signal?: AbortSignal;
625
644
  }): Promise<unknown[]>;
@@ -699,6 +718,7 @@ export declare class P2PSession extends EventEmitter {
699
718
  * Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
700
719
  * of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
701
720
  * it would lose that frame and every frame after it until a datagram happened to begin on a header.
721
+ * Payload fragments retain their original slices and are copied once, when the complete frame is delivered.
702
722
  */
703
723
  private reassemble;
704
724
  /**
@@ -46,9 +46,10 @@ export declare function receiveRecording(session: P2PSession, request: {
46
46
  signal?: AbortSignal;
47
47
  }): RecordingTransfer;
48
48
  /**
49
- * Decode a recording's frames, in arrival order, into elementary streams. Keyframes go through one
50
- * {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not open under
51
- * `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
49
+ * Decode a recording's frames, in arrival order, into elementary streams. Split pictures are assembled
50
+ * before they enter the output; an orphan continuation cannot become its own picture. Keyframes go
51
+ * through one {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not
52
+ * open under `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
52
53
  * {@link RecordingDownloadError} `undecodable` when no video frame decodes.
53
54
  */
54
55
  export declare function decodeRecording(frames: readonly RecordingFrame[], eccPrivateKeyHex: string): RecordingDownload;
@@ -438,3 +438,12 @@ export declare class SharedLiveSource {
438
438
  */
439
439
  dispose(): void;
440
440
  }
441
+ /**
442
+ * Attach a consumer, unless the caller has already abandoned the call.
443
+ *
444
+ * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
445
+ * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
446
+ * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
447
+ * not.
448
+ */
449
+ export declare function attachUnlessAborted(source: SharedLiveSource, signal?: AbortSignal): Consumer;
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Managed talkback session over P2P — the send-side twin of `live-stream.ts`. Takes AAC-LC audio from
3
- * a host, recovers frame boundaries, and paces the frames onto the wire at their own playback rate,
4
- * bracketed by the device's start/stop control frames.
2
+ * Managed talkback session over P2P — the send-side twin of `live-stream.ts`. Takes audio from a host in
3
+ * the codec the camera's speaker plays (AAC-LC ADTS, or raw AAC-ELD access units), recovers frame
4
+ * boundaries, and paces the frames onto the wire at their own playback rate, bracketed by the device's
5
+ * start/stop control frames.
5
6
  *
6
7
  * Pacing is the reason this exists rather than a bare `sendAudioFrame` loop. Each AAC-LC frame at
7
- * 16 kHz is 1024 samples — exactly 64 ms of audio — and the device plays what arrives when it arrives.
8
+ * 16 kHz is 1024 samples — exactly 64 ms of audio — and each AAC-ELD access unit is 512 samples, 32 ms.
9
+ * The device plays what arrives when it arrives.
8
10
  * A host piping a file would otherwise deliver a minute of audio in a few hundred milliseconds and
9
11
  * lose all but the tail. A live source paces itself and simply keeps the queue near-empty.
10
12
  *
@@ -17,7 +19,7 @@
17
19
  */
18
20
  import { EventEmitter } from "node:events";
19
21
  import { Writable } from "node:stream";
20
- import type { AacEncoder, StreamBudgetNotice, TalkbackHandle } from "../../core/contracts.js";
22
+ import type { AacEncoder, StreamBudgetNotice, TalkbackCodec, TalkbackHandle } from "../../core/contracts.js";
21
23
  import { type Logger } from "../../core/logger.js";
22
24
  /**
23
25
  * The subset of the session this needs — start/stop the path, push one frame, and report a frame the
@@ -66,7 +68,7 @@ export interface TalkbackOptions {
66
68
  export declare const DEFAULT_IDLE_TIMEOUT_MS = 30000;
67
69
  /**
68
70
  * A talkback session bound to one camera channel. Opens the device's path on construction via
69
- * {@link start}, then drains queued frames on a fixed 64 ms tick until {@link stop}.
71
+ * {@link start}, then drains queued frames on a tick of one frame's duration until {@link stop}.
70
72
  */
71
73
  export declare class Talkback extends EventEmitter implements TalkbackHandle {
72
74
  private readonly sink;
@@ -74,7 +76,8 @@ export declare class Talkback extends EventEmitter implements TalkbackHandle {
74
76
  private readonly reader;
75
77
  private readonly queue;
76
78
  private readonly logger;
77
- private readonly highWater;
79
+ /** Playback duration of one queued frame, the pacer's step: 64 ms for `aac-lc`, 32 ms for `aac-eld`. */
80
+ private frameMs;
78
81
  private readonly idleTimeoutMs;
79
82
  private timer?;
80
83
  private started;
@@ -94,17 +97,24 @@ export declare class Talkback extends EventEmitter implements TalkbackHandle {
94
97
  private chain;
95
98
  private readonly onAudioGap;
96
99
  constructor(sink: TalkbackSink, opts: TalkbackOptions);
100
+ /** Frames the pacing queue holds before a writable applies backpressure: {@link HIGH_WATER_MS} of this codec. */
101
+ private get highWater();
102
+ /** The codec this talkback sends, set by {@link start}. */
103
+ codec: TalkbackCodec;
97
104
  /** Frames still queued for the wire. */
98
105
  get pending(): number;
99
106
  /**
100
107
  * Open the device's talkback path and begin the pacing tick. Throws when the topology's control
101
108
  * frame could not be sent — for a HomeBase-attached camera that means the level-2 key was never
102
109
  * negotiated, which would otherwise leave a silent session that accepts audio nobody hears.
110
+ *
111
+ * `codec` is the codec the camera's speaker plays; it fixes the input this talkback accepts and the pacing step.
103
112
  */
104
- start(): this;
113
+ start(codec: TalkbackCodec): this;
105
114
  /**
106
- * Queue audio. Bytes are ADTS AAC, or PCM when an encoder was supplied; either way the input is
107
- * treated as a stream, so a chunk carrying part of a frame is held until the rest arrives.
115
+ * Queue audio in the codec this talkback sends. For `aac-lc` the bytes are ADTS, or PCM when an encoder was
116
+ * supplied, read as a stream, so a chunk carrying part of a frame is held until the rest arrives. For `aac-eld`
117
+ * each call is one whole access unit; a unit over the device's length limit is dropped with an `error`.
108
118
  *
109
119
  * Audio written after {@link end} is dropped with an `error` rather than queued: `finished` has
110
120
  * either already fired or is owed on the frames written before it, so a late chunk would either play
@@ -160,6 +170,7 @@ export declare class Talkback extends EventEmitter implements TalkbackHandle {
160
170
  /**
161
171
  * A Writable over {@link write} that withholds its callback while the queue is at the high-water
162
172
  * mark — which is what turns `pipe()` from a file into playback at speed rather than a memory spike.
173
+ * For `aac-eld` it is object-mode: each write is one access unit.
163
174
  */
164
175
  writable(): Writable;
165
176
  /** Drain an encoder's partial trailing block, so the last syllable is not lost. */
@@ -71,6 +71,8 @@ export declare class VideoFrameDecoder {
71
71
  }
72
72
  /** One whole access unit recovered from the frame (or frames) the station sent it in. */
73
73
  export interface AssembledAccessUnit {
74
+ /** Unsigned 32-bit header timestamp from the first frame of this access unit. */
75
+ sourceTimestamp: number;
74
76
  /** True when the unit's own header flagged it a keyframe — a point a consumer may begin decoding at. */
75
77
  keyframe: boolean;
76
78
  /** Frame width the unit's header declared. */
@@ -144,9 +146,10 @@ export declare class AccessUnitAssembler {
144
146
  * `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
145
147
  * frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
146
148
  * of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
147
- * carries).
149
+ * carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
150
+ * as a complete unit, even if its repeated header says keyframe.
148
151
  */
149
152
  push(payload: Buffer, decode: (payload: Buffer) => Buffer | undefined): AssembledAccessUnit[];
150
153
  /** Forget an incomplete unit, counting and reporting it. */
151
- private discard;
154
+ discard(): void;
152
155
  }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * RTC command router: drives a station over the portal's WebRTC data channel instead of P2P.
3
+ *
4
+ * The T9000 answers no P2P lookup, but it accepts the portal's control channel: sign → WS join → scall →
5
+ * SDP answer → ICE over the relay the hub grants → DTLS → SCTP data channels, with commands riding
6
+ * `WebrtcDataChannel` as portal packets (see {@link buildPortalPacket}).
7
+ *
8
+ * `set-payload` rides the `1350` SET_PAYLOAD envelope, inner `{account_id, cmd, mValue3, payload}`;
9
+ * `set-json` rides the `1700` CONTROL_PAYLOAD envelope, inner `{account_id, cmd, commandType, data}`.
10
+ * A command to the station goes on the station channel `255`; an attached device's command shares the
11
+ * station's session and keeps the device's channel. Other command kinds are refused.
12
+ *
13
+ * One session per station, reused across commands and closed after {@link IDLE_CLOSE_MS} without one;
14
+ * a live pull holds it open. Sends on a session are serialised; an ACK is correlated by its outer command
15
+ * and segment, since it carries no inner command. An ACK is not an observation of the resulting state.
16
+ *
17
+ * Live view of an attached camera rides the same session (the hub accepts one RTC session per account),
18
+ * through one shared live source per station: the station serves one camera at a time, so a second
19
+ * camera is refused until the first one's view ends.
20
+ */
21
+ import { type Command, type MediaProvider } from "../../core/contracts.js";
22
+ import type { Logger } from "../../core/logger.js";
23
+ import { RtcSession, type RtcSessionOptions } from "./session.js";
24
+ export interface RtcIdentity {
25
+ authToken: string;
26
+ userId: string;
27
+ /** The `gtoken` header value the mega session's authed HTTP calls carry. */
28
+ gtoken: string;
29
+ }
30
+ /** Where a command goes: the station's session, and whether it addresses the station or a device on it. */
31
+ export interface RtcRoute {
32
+ /** The station's signaling exchange; defaults to compact SDP. */
33
+ signalingMode?: "call" | "scall";
34
+ /** Candidate policy used by the station's exchange; defaults to relay-only. */
35
+ iceTransportPolicy?: "relay" | "all";
36
+ stationSn: string;
37
+ /**
38
+ * The station's `member.admin_user_id`, the account the session and the payload name; the logged-in
39
+ * user when the station's record carries none.
40
+ */
41
+ adminUserId?: string;
42
+ /** True for a device attached to the station, which keeps the command's own channel. */
43
+ attached: boolean;
44
+ }
45
+ /** The camera a live view is for, on its station's route. */
46
+ export interface RtcLiveRoute extends RtcRoute {
47
+ cameraSn: string;
48
+ /** The camera's `device_channel` on the station. */
49
+ channel: number;
50
+ }
51
+ export interface RtcCommandRouterDeps {
52
+ /** The logged-in session's credentials; `undefined` while logged out. */
53
+ identity: () => RtcIdentity | undefined;
54
+ /** The mega shard the account signs on (`"ie-pr"`, `"eu-pr"`, `"us-pr"`); picks the smart host. */
55
+ shard: () => string;
56
+ /** ISO country sent on the sign request (default `US`). */
57
+ country?: string;
58
+ logger?: Logger;
59
+ onError?: (e: Error) => void;
60
+ /** ffmpeg for the live still (`ffmpeg` on PATH by default). */
61
+ ffmpegPath?: string;
62
+ createSession?: (opts: RtcSessionOptions) => RtcSession;
63
+ }
64
+ export declare class RtcCommandRouter {
65
+ private readonly deps;
66
+ private readonly sessions;
67
+ private readonly lives;
68
+ private readonly warm;
69
+ constructor(deps: RtcCommandRouterDeps);
70
+ dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
71
+ /**
72
+ * The media of a camera on a T9000: live video, a readable feed and a live still over the station's
73
+ * session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
74
+ */
75
+ mediaProviderFor(route: RtcLiveRoute): MediaProvider;
76
+ /**
77
+ * Tear down every station session (logout / shutdown). Each live source is disposed before its session
78
+ * closes, so its consumers end cleanly and its stop still rides the session.
79
+ */
80
+ close(): void;
81
+ /**
82
+ * Hand `use` the station's live source for this camera, in the same turn the source is created or reused,
83
+ * so a consumer it attaches is counted before any other caller looks.
84
+ *
85
+ * The station streams one camera at a time. Another camera whose source has consumers refuses this one,
86
+ * before the session is awaited and again after; one with none (never attached or lingering)
87
+ * holds nothing a viewer needs, so it is disposed and the slot taken. The first caller's
88
+ * {@link SharedSourceHints} configure a new source.
89
+ */
90
+ private withLiveSource;
91
+ /** Refuse a camera while another camera on its station has consumers. */
92
+ private refuseIfWatched;
93
+ private newLiveSource;
94
+ /**
95
+ * Completes on the command ACK or a correlated control-result notification. The notification must
96
+ * match the request channel, parameter and segment and contain a structured payload. Receipt does
97
+ * not establish physical actuation. The command is sent once, bounded by {@link ACK_TIMEOUT_MS}.
98
+ */
99
+ private sendAwaitAck;
100
+ private stationSession;
101
+ /**
102
+ * One bounded bring-up: the session is up when its command channel opens, and it fails, with the
103
+ * session closed and every listener gone, when `connect()` throws, the session closes first, or
104
+ * {@link CONNECT_TIMEOUT_MS} passes. A single promise, so an early failure cannot leave a second one
105
+ * rejecting unheard.
106
+ */
107
+ private bringUp;
108
+ /**
109
+ * Hold a station's command session open: one hold per station, its idle-close ({@link IDLE_CLOSE_MS})
110
+ * suppressed, {@link PORTAL_KEEPALIVE} sent every {@link PORTAL_KEEPALIVE_MS}, and the session dropped
111
+ * on a tick that finds the previous keepalive unechoed. A closed session ends the hold.
112
+ *
113
+ * Answers an idempotent disposer that releases the hold. Throws when no account is logged in.
114
+ */
115
+ keepWarm(route: RtcRoute): () => void;
116
+ /**
117
+ * Bring a station session up for a {@link keepWarm} hold and start its keepalive. A bring-up that fails,
118
+ * or a hold disposed before it completes, clears the hold and arms no timer. Each tick first checks
119
+ * that the hub echoed the previous keepalive, and drops the session when it did not. The keepalive
120
+ * stops when the session closes, which also clears the hold so the next command or `keepWarm`
121
+ * rebuilds cleanly.
122
+ */
123
+ private warmUp;
124
+ /** Stop a hold's keepalive and remove its listeners from the session it watches. */
125
+ private unwatch;
126
+ private touch;
127
+ private drop;
128
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The live pull of one camera behind a **HomeBase S1 Pro (T9000)**, over the station's control session.
3
+ *
4
+ * Wire, as the portal sends it on the command data channel:
5
+ *
6
+ * - Start is inner `cmd 1003` in a `1350` SET_PAYLOAD on the station channel `255`. The header byte the
7
+ * codec names `isResponse` carries the portal's `streamId`, `1` for a live view, and `chn_list` is an
8
+ * array of `{index, chn, sensor, isUps, isClicked}` objects. Stop is inner `cmd 1004`, the same way.
9
+ * - Video arrives on the live link as portal packets `1300` on channel `100 + play id`; a single-camera
10
+ * view is play id `1`, whatever the camera's own channel. Each body is a fixed 22-byte media header,
11
+ * then the Annex B video it declares. See {@link parseMediaBody}.
12
+ * - A raw, unframed keepalive (a 20-byte prefix and a bare `XZYH 1139`) goes out every ~29 s; the hub
13
+ * echoes it.
14
+ *
15
+ * The station serves one live view at a time on that play slot, so the router builds at most one pull per
16
+ * station. This is a {@link LiveStreamHandle}: fan-out, backpressure and the start deadline are the
17
+ * shared live source's.
18
+ */
19
+ import { EventEmitter } from "node:events";
20
+ import type { LiveStreamHandle } from "../../core/contracts.js";
21
+ import type { Logger } from "../../core/logger.js";
22
+ import type { RtcSession } from "./session.js";
23
+ import { type SegmentCounter } from "./portal-packet.js";
24
+ /**
25
+ * Read a `1300` body: a 22-byte media header (u32 LE video length at 0, stream type at 5, u16 LE width
26
+ * and height at 10 and 12), then the Annex B video. This is the layout the portal's own HEVC worker
27
+ * parses. Answers `undefined` for a short body, a stream that is not HEVC, or a declared length the body
28
+ * does not hold.
29
+ */
30
+ export declare function parseMediaBody(body: Buffer): {
31
+ data: Buffer;
32
+ width: number;
33
+ height: number;
34
+ } | undefined;
35
+ export interface RtcLiveOptions {
36
+ session: RtcSession;
37
+ /** The session's segment counter, shared with the command router so segments never collide. */
38
+ seg: SegmentCounter;
39
+ stationSn: string;
40
+ /** The camera's `device_channel` on the station. */
41
+ channel: number;
42
+ /** The station's `member.admin_user_id`. */
43
+ accountId: string;
44
+ logger?: Logger;
45
+ }
46
+ /**
47
+ * One camera's live pull. `start()` sends the start; a non-zero start ACK or a closed session ends it with
48
+ * `error`, and `stop()` sends the stop. Frames are `h265` Annex B, keyframes being IDRs.
49
+ */
50
+ export declare class RtcLiveStream extends EventEmitter implements LiveStreamHandle {
51
+ private readonly opts;
52
+ private running;
53
+ private startSegment;
54
+ private keepalive?;
55
+ private width;
56
+ private height;
57
+ private readonly onMedia;
58
+ private readonly onCommand;
59
+ private readonly onClose;
60
+ constructor(opts: RtcLiveOptions);
61
+ private get tag();
62
+ start(): this;
63
+ stop(): void;
64
+ private teardown;
65
+ private fail;
66
+ /** The start's ACK: a non-zero result code ends the pull. */
67
+ private onCommandFrame;
68
+ private onMediaFrame;
69
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The WebRTC peer for a T9000 session: libdatachannel through `node-datachannel`, driven the way the
3
+ * portal drives a browser's RTCPeerConnection.
4
+ *
5
+ * The hub is the offerer: it sends its SDP once it has granted the session, and the peer answers. Six
6
+ * data channels are declared in the portal's order (`WebrtcDataChannel` first, the command channel,
7
+ * then audio, idr, video, notify, download). Every one carries PTCS-framed portal packets; which logical
8
+ * channel a reassembled frame belongs to is the PTCS header's channel, not the data channel it rode on.
9
+ *
10
+ * ICE defaults to relay-only. The T9000 grants a TURN allocation in its `scall 100` reply and completes DTLS only
11
+ * over the relay pair; on a host pair ICE connects and the DTLS handshake never completes. The peer
12
+ * plays the DTLS client (`active`) and opens its channels on the client's even SCTP stream ids, which
13
+ * is what the hub pairs with.
14
+ *
15
+ * The native peer is injectable.
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ import type { PeerConnection, RtcConfig } from "node-datachannel";
19
+ import { type Logger } from "../../core/logger.js";
20
+ export interface TurnConfig {
21
+ turn_addr: string;
22
+ turn_port: number;
23
+ turn_user: string;
24
+ turn_password: string;
25
+ alt_turn_addr?: string;
26
+ alt_turn_port?: number;
27
+ }
28
+ export type NativePeerFactory = (name: string, config: RtcConfig) => PeerConnection;
29
+ export interface RtcPeerOptions {
30
+ createPeer?: NativePeerFactory;
31
+ /** Candidate policy for the station's exchange; the default is relay-only. */
32
+ iceTransportPolicy?: "relay" | "all";
33
+ logger?: Logger;
34
+ }
35
+ export interface RtcPeerEvents {
36
+ commandChannelOpen: [];
37
+ /** The command channel went away while the peer itself may still be up. */
38
+ commandChannelClosed: [];
39
+ data: [frame: Buffer, linkType: number];
40
+ /** The hub echoed the portal keepalive on the command channel. */
41
+ keepaliveEcho: [];
42
+ iceCandidate: [candidate: string];
43
+ iceGatheringComplete: [];
44
+ connectionState: [state: string];
45
+ error: [err: Error];
46
+ }
47
+ /** The portal's channel list; index 0 is the command channel. */
48
+ export declare const DATA_CHANNEL_LABELS: readonly ["WebrtcDataChannel", "audio", "idr", "video", "notify", "download"];
49
+ export declare const COMMAND_CHANNEL: "WebrtcDataChannel";
50
+ export declare class RtcPeer extends EventEmitter<RtcPeerEvents> {
51
+ private readonly opts;
52
+ private pc?;
53
+ private readonly channels;
54
+ private framer?;
55
+ private readonly wireTally;
56
+ private commandOpen;
57
+ private remoteSet;
58
+ private remoteMid;
59
+ private handlingOffer;
60
+ private channelsCreated;
61
+ private gatheringDone;
62
+ /** Set by the framer's wire callback when the native channel refused a packet mid-send. */
63
+ private wireSendFailed;
64
+ private readonly pending;
65
+ private localAnswer?;
66
+ private readonly logger;
67
+ constructor(opts?: RtcPeerOptions);
68
+ /** Create the native peer on the relay the hub granted in `scall 100`. */
69
+ init(turn: TurnConfig): Promise<void>;
70
+ /**
71
+ * The hub offered: apply the offer, declare the channels, return the answer SDP to signal back.
72
+ *
73
+ * The offer is pinned to `passive` before it is applied, so the native peer has one legal answer,
74
+ * `active`: the role is fixed where it is decided, since rewriting the answer afterwards would change
75
+ * only what is announced. The offer goes in before the channels are declared: `createDataChannel` on
76
+ * a peer without a remote description starts its own negotiation and leaves it in `have-local-offer`.
77
+ * The data channels need no m-line of their own; the offer's SCTP m-line carries them. Only an ANSWER
78
+ * is taken as the local description: one read in `have-local-offer` is an offer.
79
+ */
80
+ handleRemoteOffer(offerSdp: string): Promise<string>;
81
+ addRemoteCandidate(candidate: string): void;
82
+ /**
83
+ * Send bytes on the command channel as they are, without PTCS framing; false when it isn't open. The
84
+ * portal's data-channel keepalive rides the wire unframed.
85
+ */
86
+ sendRaw(bytes: Buffer): boolean;
87
+ /**
88
+ * Send one portal packet on the command channel, through the framer. False when the channel is not
89
+ * usable or the native send refused a wire packet, so a dropped frame does not read as success.
90
+ */
91
+ sendCommand(portalPacket: Buffer): boolean;
92
+ /** Tear the peer down. A pending answer is rejected, so a `handleRemoteOffer` in flight settles. */
93
+ close(): void;
94
+ private acceptsCandidate;
95
+ private createChannels;
96
+ /**
97
+ * Wire one data channel. Every channel feeds the framer once the command channel has opened it; a
98
+ * message before that has nothing to reassemble into and is dropped. The hub's echo of the portal
99
+ * keepalive on the command channel is unframed and surfaces as `keepaliveEcho` instead.
100
+ */
101
+ private wireChannel;
102
+ private addNow;
103
+ private flushPending;
104
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The portal's data-channel keepalive for an RTC session.
3
+ *
4
+ * {@link PORTAL_KEEPALIVE} is the 36 bytes the portal sends unframed on the data channel — a 20-byte
5
+ * prefix then a bare `XZYH 1139`, no portal packet header — and the hub echoes it. Sent every
6
+ * {@link PORTAL_KEEPALIVE_MS}, it stops the hub idling the session out.
7
+ */
8
+ /** The portal's 36-byte data-channel keepalive: a 20-byte prefix and a bare `XZYH 1139`. */
9
+ export declare const PORTAL_KEEPALIVE: Buffer<ArrayBuffer>;
10
+ /** How often {@link PORTAL_KEEPALIVE} goes out on an RTC session. */
11
+ export declare const PORTAL_KEEPALIVE_MS = 29000;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The command framing a HomeBase S1 Pro (T9000) speaks over its WebRTC data channel — the wire the
3
+ * security.eufy.com web client builds, reproduced byte-exact.
4
+ *
5
+ * A packet is a 16-byte header behind the ASCII magic `XZYH`, then a JSON body, sent in the clear (the
6
+ * DTLS channel is the encryption). Replies come back with `isResponse = 1`, the same `segment` the
7
+ * request carried, and a body whose first four bytes are a little-endian result code.
8
+ *
9
+ * Every offset is the portal bundle's header builder and parser (`Gr()` / `Qr()`) and its
10
+ * `worker_sctp_send`.
11
+ */
12
+ export declare const PORTAL_HEADER_LENGTH = 16;
13
+ /** The SET_PAYLOAD envelope. */
14
+ export declare const PORTAL_CMD_SET_PAYLOAD = 1350;
15
+ /** The channel a station-wide command is addressed to. */
16
+ export declare const PORTAL_STATION_CHANNEL = 255;
17
+ /** Portal link types — which logical channel a frame belongs to (`worker_sctp_send`). */
18
+ export declare const PortalLinkType: {
19
+ /** Command channel: requests out, acknowledgements back. */
20
+ readonly COMMAND: 1;
21
+ /** Station-originated frames: pushes, notify payloads, control results. */
22
+ readonly NOTIFY: 3;
23
+ /** Live video. */
24
+ readonly LIVE: 5;
25
+ };
26
+ export interface PortalHeader {
27
+ commandId: number;
28
+ paramLength: number;
29
+ segment: number;
30
+ channel: number;
31
+ isResponse: number;
32
+ devType: number;
33
+ }
34
+ export interface PortalRequest {
35
+ commandId: number;
36
+ channel: number;
37
+ /** 1..255, wraps; echoed by the reply so it can be matched. */
38
+ segment: number;
39
+ payload: Record<string, unknown>;
40
+ isResponse?: number;
41
+ devType?: number;
42
+ }
43
+ export interface PortalResponse {
44
+ commandId: number;
45
+ channel: number;
46
+ segment: number;
47
+ isResponse: number;
48
+ linkType: number;
49
+ /** Command-channel replies only: the int32 result code (0 = ok). */
50
+ errCode?: number;
51
+ /** Notify frames: the nested `cmd` the JSON body carries. */
52
+ cmd?: number;
53
+ data?: unknown;
54
+ }
55
+ /** The 16-byte header (portal `Gr()`). */
56
+ export declare function buildPortalHeader(commandId: number, paramLength: number, channel: number, segment: number, isResponse?: number, devType?: number): Buffer;
57
+ /** Parse the header (portal `Qr()`); `undefined` when the magic is missing or the buffer is short. */
58
+ export declare function parsePortalHeader(buf: Buffer): PortalHeader | undefined;
59
+ /** Whether a buffer starts a portal packet. */
60
+ export declare function isPortalPacket(buf: Buffer): boolean;
61
+ /** A complete request packet: header + body. */
62
+ export declare function buildPortalPacket(req: PortalRequest): Buffer;
63
+ /**
64
+ * Parse an inbound packet. On the command channel the body opens with the int32 result code and may
65
+ * carry JSON after it; on the notify channel the whole body is JSON with a nested `cmd`.
66
+ */
67
+ export declare function parsePortalPacket(buf: Buffer, linkType?: number): PortalResponse | undefined;
68
+ /** A 1..255 segment counter that never yields 0 (the portal reserves it). */
69
+ export declare class SegmentCounter {
70
+ private current;
71
+ next(): number;
72
+ }