@mega-yfue/eufy-sdk 0.5.0-beta.26 → 0.5.0-beta.28
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/dist/client/eufy-mega.d.ts +29 -0
- package/dist/index.js +445 -70
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/types.d.ts +6 -0
- package/dist/transport/abortable.d.ts +8 -0
- package/dist/transport/p2p/command-router.d.ts +0 -20
- package/dist/transport/p2p/shared-live-source.d.ts +9 -0
- package/dist/transport/rtc/command-router.d.ts +39 -5
- package/dist/transport/rtc/live.d.ts +69 -0
- package/dist/transport/rtc/peer.d.ts +5 -0
- package/dist/transport/rtc/portal-packet.d.ts +6 -0
- package/dist/transport/rtc/ptcs-framer.d.ts +3 -2
- package/dist/transport/rtc/session.d.ts +5 -1
- package/package.json +1 -1
|
@@ -247,6 +247,12 @@ export interface AvailabilityContext {
|
|
|
247
247
|
homeBaseAttached?: boolean;
|
|
248
248
|
/** Serial of the covering station, when the device record names one. */
|
|
249
249
|
stationSerial?: string;
|
|
250
|
+
/**
|
|
251
|
+
* The vendor DeviceType of the station the device hangs off, when it is attached
|
|
252
|
+
* and the station's record states one. Answers which station family a command addressed to an
|
|
253
|
+
* attached device is carried through.
|
|
254
|
+
*/
|
|
255
|
+
stationDeviceType?: number;
|
|
250
256
|
}
|
|
251
257
|
export interface CommandContext extends AvailabilityContext {
|
|
252
258
|
/** Device channel (0 for standalone, `device_channel` on a HomeBase). */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settle `work` as it settles, or reject the moment `signal` aborts, whichever comes first.
|
|
3
|
+
*
|
|
4
|
+
* The underlying wait is left to finish on its own: it may be shared work other callers are waiting on, so a
|
|
5
|
+
* caller abandoning its own call does not cancel the work itself. This abandons WAITING, which is the only
|
|
6
|
+
* part that belonged to the caller.
|
|
7
|
+
*/
|
|
8
|
+
export declare function abortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P2P command router — the transport-side owner of the ThroughTek PPCS sessions and every wire
|
|
3
|
-
* operation over them: opening sessions, resolving a serial to its session + routing params,
|
|
4
|
-
* mapping a transport-neutral {@link Command} to a concrete frame (encryption level, wire shape),
|
|
5
|
-
* the fire-and-forget control senders, request/reply queries, and the media provider.
|
|
6
|
-
*
|
|
7
|
-
* Layering: this module knows P2P bytes; it does NOT know capabilities. Frame → semantic-event
|
|
8
|
-
* decoding is a model concern, so raw frames are handed back to the client via {@link P2PRouterDeps.onFrame}
|
|
9
|
-
* (the client gates them on device capabilities and emits typed events). This keeps transport free of
|
|
10
|
-
* any `model/` import — the capability↔transport decorrelation invariant.
|
|
11
|
-
*/
|
|
12
1
|
import type { MegaHttpClient } from "../http/mega-client.js";
|
|
13
2
|
import type { EufyDevice } from "../../core/types.js";
|
|
14
3
|
import type { Command, AutoLockSnapshot, MediaProvider, SharedSourceHints, AbortableCall } from "../../core/contracts.js";
|
|
@@ -461,15 +450,6 @@ export declare class P2PCommandRouter {
|
|
|
461
450
|
* A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
|
|
462
451
|
* unaffected.
|
|
463
452
|
*/
|
|
464
|
-
/**
|
|
465
|
-
* Attach a consumer, unless the caller has already abandoned the call.
|
|
466
|
-
*
|
|
467
|
-
* The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
|
|
468
|
-
* somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
|
|
469
|
-
* lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
|
|
470
|
-
* not.
|
|
471
|
-
*/
|
|
472
|
-
private attachUnlessAborted;
|
|
473
453
|
/**
|
|
474
454
|
* Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
|
|
475
455
|
* on a session goes with it, and a media session opened for this source alone is closed: nothing else
|
|
@@ -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;
|
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
* A command to the station goes on the station channel `255`; an attached device's command shares the
|
|
11
11
|
* station's session and keeps the device's channel. Other command kinds are refused.
|
|
12
12
|
*
|
|
13
|
-
* One session per station, reused across commands and closed after {@link IDLE_CLOSE_MS} without one
|
|
14
|
-
* Sends on a session are serialised; an ACK is correlated by its outer command
|
|
15
|
-
* carries no inner command. An ACK is not an observation of the resulting state.
|
|
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.
|
|
16
20
|
*/
|
|
17
|
-
import type
|
|
21
|
+
import { type Command, type MediaProvider } from "../../core/contracts.js";
|
|
18
22
|
import type { Logger } from "../../core/logger.js";
|
|
19
23
|
import { RtcSession, type RtcSessionOptions } from "./session.js";
|
|
20
24
|
export interface RtcIdentity {
|
|
@@ -38,6 +42,12 @@ export interface RtcRoute {
|
|
|
38
42
|
/** True for a device attached to the station, which keeps the command's own channel. */
|
|
39
43
|
attached: boolean;
|
|
40
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
|
+
}
|
|
41
51
|
export interface RtcCommandRouterDeps {
|
|
42
52
|
/** The logged-in session's credentials; `undefined` while logged out. */
|
|
43
53
|
identity: () => RtcIdentity | undefined;
|
|
@@ -47,15 +57,39 @@ export interface RtcCommandRouterDeps {
|
|
|
47
57
|
country?: string;
|
|
48
58
|
logger?: Logger;
|
|
49
59
|
onError?: (e: Error) => void;
|
|
60
|
+
/** ffmpeg for the live still (`ffmpeg` on PATH by default). */
|
|
61
|
+
ffmpegPath?: string;
|
|
50
62
|
createSession?: (opts: RtcSessionOptions) => RtcSession;
|
|
51
63
|
}
|
|
52
64
|
export declare class RtcCommandRouter {
|
|
53
65
|
private readonly deps;
|
|
54
66
|
private readonly sessions;
|
|
67
|
+
private readonly lives;
|
|
55
68
|
constructor(deps: RtcCommandRouterDeps);
|
|
56
69
|
dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
|
|
57
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The media of a camera on a T9000: live video, a readable feed and a live still over the station's
|
|
72
|
+
* session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
|
|
73
|
+
*/
|
|
74
|
+
mediaProviderFor(route: RtcLiveRoute): MediaProvider;
|
|
75
|
+
/**
|
|
76
|
+
* Tear down every station session (logout / shutdown). Each live source is disposed before its session
|
|
77
|
+
* closes, so its consumers end cleanly and its stop still rides the session.
|
|
78
|
+
*/
|
|
58
79
|
close(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Hand `use` the station's live source for this camera, in the same turn the source is created or reused,
|
|
82
|
+
* so a consumer it attaches is counted before any other caller looks.
|
|
83
|
+
*
|
|
84
|
+
* The station streams one camera at a time. Another camera whose source has consumers refuses this one,
|
|
85
|
+
* before the session is awaited and again after; one with none (never attached or lingering)
|
|
86
|
+
* holds nothing a viewer needs, so it is disposed and the slot taken. The first caller's
|
|
87
|
+
* {@link SharedSourceHints} configure a new source.
|
|
88
|
+
*/
|
|
89
|
+
private withLiveSource;
|
|
90
|
+
/** Refuse a camera while another camera on its station has consumers. */
|
|
91
|
+
private refuseIfWatched;
|
|
92
|
+
private newLiveSource;
|
|
59
93
|
/**
|
|
60
94
|
* Completes on the command ACK or a correlated control-result notification. The notification must
|
|
61
95
|
* match the request channel, parameter and segment and contain a structured payload. Receipt does
|
|
@@ -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
|
+
}
|
|
@@ -77,6 +77,11 @@ export declare class RtcPeer extends EventEmitter<RtcPeerEvents> {
|
|
|
77
77
|
*/
|
|
78
78
|
handleRemoteOffer(offerSdp: string): Promise<string>;
|
|
79
79
|
addRemoteCandidate(candidate: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* Send bytes on the command channel as they are, without PTCS framing; false when it isn't open. The
|
|
82
|
+
* portal's data-channel keepalive rides the wire unframed.
|
|
83
|
+
*/
|
|
84
|
+
sendRaw(bytes: Buffer): boolean;
|
|
80
85
|
/**
|
|
81
86
|
* Send one portal packet on the command channel, through the framer. False when the channel is not
|
|
82
87
|
* usable or the native send refused a wire packet, so a dropped frame does not read as success.
|
|
@@ -10,12 +10,18 @@
|
|
|
10
10
|
* `worker_sctp_send`.
|
|
11
11
|
*/
|
|
12
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;
|
|
13
17
|
/** Portal link types — which logical channel a frame belongs to (`worker_sctp_send`). */
|
|
14
18
|
export declare const PortalLinkType: {
|
|
15
19
|
/** Command channel: requests out, acknowledgements back. */
|
|
16
20
|
readonly COMMAND: 1;
|
|
17
21
|
/** Station-originated frames: pushes, notify payloads, control results. */
|
|
18
22
|
readonly NOTIFY: 3;
|
|
23
|
+
/** Live video. */
|
|
24
|
+
readonly LIVE: 5;
|
|
19
25
|
};
|
|
20
26
|
export interface PortalHeader {
|
|
21
27
|
commandId: number;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 0 "PTCS"
|
|
9
9
|
* 4 u8 3 constant
|
|
10
|
-
* 5 u8 channel the logical channel the frame belongs to: 0 command, 2 notify
|
|
10
|
+
* 5 u8 channel the logical channel the frame belongs to: 0 command, 2 notify, 5 live
|
|
11
11
|
* 6 u16 sequence a frame counter: every packet of a frame carries the same value, and it
|
|
12
12
|
* steps once per frame
|
|
13
13
|
* 8 u32 frame id one value per frame, shared by all its packets — the portal uses a ms clock
|
|
@@ -28,8 +28,9 @@ export declare const PTCS_DEFAULT_PAYLOAD_BYTES = 1000;
|
|
|
28
28
|
export declare const PtcsChannel: {
|
|
29
29
|
readonly COMMAND: 0;
|
|
30
30
|
readonly NOTIFY: 2;
|
|
31
|
+
readonly LIVE: 5;
|
|
31
32
|
};
|
|
32
|
-
/** The link type a received frame belongs to: notify on
|
|
33
|
+
/** The link type a received frame belongs to: notify and live on their channels, command otherwise. */
|
|
33
34
|
export declare function linkTypeForChannel(channel: number): number;
|
|
34
35
|
/** How long a partly received frame is kept before it is dropped. */
|
|
35
36
|
export declare const PTCS_STALE_MS = 15000;
|
|
@@ -31,8 +31,10 @@ export interface RtcSessionEvents {
|
|
|
31
31
|
connected: [];
|
|
32
32
|
close: [];
|
|
33
33
|
error: [err: Error];
|
|
34
|
-
/** A reassembled frame: portal packet bytes + the link type it arrived on. */
|
|
34
|
+
/** A reassembled command or notify frame: portal packet bytes + the link type it arrived on. */
|
|
35
35
|
commandData: [frame: Buffer, linkType: number];
|
|
36
|
+
/** A reassembled live-video frame. */
|
|
37
|
+
mediaData: [frame: Buffer];
|
|
36
38
|
}
|
|
37
39
|
export declare class RtcSession extends EventEmitter<RtcSessionEvents> {
|
|
38
40
|
private readonly opts;
|
|
@@ -57,6 +59,8 @@ export declare class RtcSession extends EventEmitter<RtcSessionEvents> {
|
|
|
57
59
|
connect(): Promise<void>;
|
|
58
60
|
/** Send one portal packet; false when the command channel isn't open. */
|
|
59
61
|
sendCommand(portalPacket: Buffer): boolean;
|
|
62
|
+
/** Send unframed bytes on the command channel; false when it isn't open. */
|
|
63
|
+
sendRaw(bytes: Buffer): boolean;
|
|
60
64
|
/**
|
|
61
65
|
* Hang up and tear both sides down; `close` fires once, here if nothing announced it before. A
|
|
62
66
|
* `connect()` still waiting for the signalling auth is rejected and its timer stopped.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.28",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|