@mega-yfue/eufy-sdk 0.5.0-beta.25 → 0.5.0-beta.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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;
@@ -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. */
@@ -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 and segment, since it
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 { Command } from "../../core/contracts.js";
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
- /** Tear down every station session (logout / shutdown). */
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 the notify channel, command otherwise. */
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.25",
3
+ "version": "0.5.0-beta.27",
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",