@mega-yfue/eufy-sdk 0.5.0-beta.26 → 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.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // dist/client/eufy-mega.js
8
- import { EventEmitter as EventEmitter12 } from "node:events";
8
+ import { EventEmitter as EventEmitter13 } from "node:events";
9
9
 
10
10
  // dist/transport/http/mega-client.js
11
11
  import { createHash as createHash3 } from "node:crypto";
@@ -14514,6 +14514,19 @@ function decodeMapFrame(frame, codec) {
14514
14514
  return READERS.get(frame.channelId)?.(frame.payload, codec);
14515
14515
  }
14516
14516
 
14517
+ // dist/transport/abortable.js
14518
+ function abortable(work, signal) {
14519
+ if (!signal)
14520
+ return work;
14521
+ signal.throwIfAborted();
14522
+ return Promise.race([
14523
+ work,
14524
+ new Promise((_resolve, reject) => {
14525
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
14526
+ })
14527
+ ]);
14528
+ }
14529
+
14517
14530
  // dist/transport/p2p/command-router.js
14518
14531
  import { setTimeout as sleep2 } from "node:timers/promises";
14519
14532
 
@@ -17860,6 +17873,18 @@ function startCodeBegin(buf, payloadOffset) {
17860
17873
  return threeByte - 1;
17861
17874
  return threeByte;
17862
17875
  }
17876
+ function hasIdr(buf, codec) {
17877
+ for (const p of nalStarts(buf)) {
17878
+ const b = buf[p];
17879
+ if (codec === "h265") {
17880
+ const t = b >> 1 & 63;
17881
+ if (t === 19 || t === 20)
17882
+ return true;
17883
+ } else if ((b & 31) === 5)
17884
+ return true;
17885
+ }
17886
+ return false;
17887
+ }
17863
17888
  var MAX_CODED_DIMENSION = 32768;
17864
17889
  var H264_CHROMA_PROFILES = /* @__PURE__ */ new Set([100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135, 144]);
17865
17890
  var CHROMA_SUBSAMPLING = {
@@ -19630,6 +19655,14 @@ var SharedLiveSource = class {
19630
19655
  this.opts.onIdle?.();
19631
19656
  }
19632
19657
  };
19658
+ function attachUnlessAborted(source, signal) {
19659
+ const consumer = source.attach();
19660
+ if (signal?.aborted) {
19661
+ consumer.detach();
19662
+ signal.throwIfAborted();
19663
+ }
19664
+ return consumer;
19665
+ }
19633
19666
 
19634
19667
  // dist/transport/p2p/session-manager.js
19635
19668
  var SessionSupersededError = class extends Error {
@@ -20986,17 +21019,6 @@ function stationChannels(devices) {
20986
21019
 
20987
21020
  // dist/transport/p2p/command-router.js
20988
21021
  var DIRECT_CMD_SENDS = 5;
20989
- function abortable(work, signal) {
20990
- if (!signal)
20991
- return work;
20992
- signal.throwIfAborted();
20993
- return Promise.race([
20994
- work,
20995
- new Promise((_resolve, reject) => {
20996
- signal.addEventListener("abort", () => reject(signal.reason), { once: true });
20997
- })
20998
- ]);
20999
- }
21000
21022
  var LEVEL2_GRACE_MS = 25e3;
21001
21023
  var LEVEL2_SETTLE_MS = 8e3;
21002
21024
  var P2P_STATION_WAITS = {
@@ -21488,11 +21510,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21488
21510
  },
21489
21511
  live: async (opts) => {
21490
21512
  const source = await this.sharedLiveSourceFor(sn, opts, true);
21491
- return this.attachUnlessAborted(source, opts?.signal);
21513
+ return attachUnlessAborted(source, opts?.signal);
21492
21514
  },
21493
21515
  openReadable: async (opts) => {
21494
21516
  const source = await this.sharedLiveSourceFor(sn, opts ?? {}, true);
21495
- return openReadableFromConsumer(this.attachUnlessAborted(source, opts?.signal), opts);
21517
+ return openReadableFromConsumer(attachUnlessAborted(source, opts?.signal), opts);
21496
21518
  },
21497
21519
  recordFragments: (opts) => this.recordFragments(sn, opts),
21498
21520
  talkback: (opts) => this.openTalkback(sn, opts),
@@ -21860,22 +21882,6 @@ var P2PCommandRouter = class _P2PCommandRouter {
21860
21882
  * A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
21861
21883
  * unaffected.
21862
21884
  */
21863
- /**
21864
- * Attach a consumer, unless the caller has already abandoned the call.
21865
- *
21866
- * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
21867
- * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
21868
- * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
21869
- * not.
21870
- */
21871
- attachUnlessAborted(source, signal) {
21872
- const consumer = source.attach();
21873
- if (signal?.aborted) {
21874
- consumer.detach();
21875
- signal.throwIfAborted();
21876
- }
21877
- return consumer;
21878
- }
21879
21885
  /**
21880
21886
  * Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
21881
21887
  * on a session goes with it, and a media session opened for this source alone is closed: nothing else
@@ -22645,20 +22651,21 @@ var P2PCommandRouter = class _P2PCommandRouter {
22645
22651
  }
22646
22652
  };
22647
22653
 
22648
- // dist/transport/rtc/session.js
22649
- import { EventEmitter as EventEmitter9 } from "node:events";
22650
-
22651
- // dist/transport/rtc/peer.js
22654
+ // dist/transport/rtc/live.js
22652
22655
  import { EventEmitter as EventEmitter7 } from "node:events";
22653
22656
 
22654
22657
  // dist/transport/rtc/portal-packet.js
22655
22658
  var MAGIC2 = Buffer.from("XZYH", "ascii");
22656
22659
  var PORTAL_HEADER_LENGTH = 16;
22660
+ var PORTAL_CMD_SET_PAYLOAD = 1350;
22661
+ var PORTAL_STATION_CHANNEL = 255;
22657
22662
  var PortalLinkType = {
22658
22663
  /** Command channel: requests out, acknowledgements back. */
22659
22664
  COMMAND: 1,
22660
22665
  /** Station-originated frames: pushes, notify payloads, control results. */
22661
- NOTIFY: 3
22666
+ NOTIFY: 3,
22667
+ /** Live video. */
22668
+ LIVE: 5
22662
22669
  };
22663
22670
  function encodeLe(value, width) {
22664
22671
  const buf = Buffer.alloc(width);
@@ -22750,6 +22757,159 @@ var SegmentCounter = class {
22750
22757
  }
22751
22758
  };
22752
22759
 
22760
+ // dist/transport/rtc/live.js
22761
+ var LIVE_START = 1003;
22762
+ var LIVE_STOP = 1004;
22763
+ var LIVE_MEDIA = 1300;
22764
+ var LIVE_MEDIA_CHANNEL = 101;
22765
+ var LIVE_STREAM_ID = 1;
22766
+ var KEEPALIVE_MS = 29e3;
22767
+ var KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
22768
+ var MEDIA_HEADER_LENGTH = 22;
22769
+ var STREAM_TYPE_HEVC = 1;
22770
+ function parseMediaBody(body) {
22771
+ if (body.length < MEDIA_HEADER_LENGTH || body[5] !== STREAM_TYPE_HEVC)
22772
+ return void 0;
22773
+ const length = body.readUInt32LE(0);
22774
+ if (body.length < MEDIA_HEADER_LENGTH + length)
22775
+ return void 0;
22776
+ return {
22777
+ data: body.subarray(MEDIA_HEADER_LENGTH, MEDIA_HEADER_LENGTH + length),
22778
+ width: body.readUInt16LE(10),
22779
+ height: body.readUInt16LE(12)
22780
+ };
22781
+ }
22782
+ var RtcLiveStream = class extends EventEmitter7 {
22783
+ opts;
22784
+ running = false;
22785
+ startSegment = -1;
22786
+ keepalive;
22787
+ width = 0;
22788
+ height = 0;
22789
+ onMedia = (frame) => this.onMediaFrame(frame);
22790
+ onCommand = (frame, linkType) => this.onCommandFrame(frame, linkType);
22791
+ onClose = () => this.fail(new Error(`rtc live ${this.tag}: session closed`));
22792
+ constructor(opts) {
22793
+ super();
22794
+ this.opts = opts;
22795
+ }
22796
+ get tag() {
22797
+ return `${this.opts.stationSn}#${this.opts.channel}`;
22798
+ }
22799
+ start() {
22800
+ if (this.running)
22801
+ return this;
22802
+ const { session, seg, accountId } = this.opts;
22803
+ this.running = true;
22804
+ session.on("mediaData", this.onMedia);
22805
+ session.on("commandData", this.onCommand);
22806
+ session.on("close", this.onClose);
22807
+ this.startSegment = seg.next();
22808
+ const sent = session.sendCommand(buildPortalPacket({
22809
+ commandId: PORTAL_CMD_SET_PAYLOAD,
22810
+ channel: PORTAL_STATION_CHANNEL,
22811
+ segment: this.startSegment,
22812
+ isResponse: LIVE_STREAM_ID,
22813
+ payload: {
22814
+ account_id: accountId,
22815
+ cmd: LIVE_START,
22816
+ payload: {
22817
+ ClientOS: "WEB",
22818
+ entrytype: 0,
22819
+ camera_type: 0,
22820
+ key: "",
22821
+ msg_id: 116,
22822
+ audio_chn: -1,
22823
+ streamtype: 2,
22824
+ stitch_mode: 1,
22825
+ chn_list: [{ index: 0, chn: this.opts.channel, sensor: 0, isUps: 0, isClicked: true }],
22826
+ pip_cord: { x1: 0, y1: 0, x2: 0, y2: 0 },
22827
+ station_video_type: 6,
22828
+ play_id: 1
22829
+ }
22830
+ }
22831
+ }));
22832
+ if (!sent) {
22833
+ queueMicrotask(() => this.fail(new Error(`rtc live ${this.tag}: command channel not open`)));
22834
+ return this;
22835
+ }
22836
+ this.keepalive = setInterval(() => {
22837
+ if (!session.sendRaw(KEEPALIVE))
22838
+ this.opts.logger?.warn?.(`[rtc] ${this.tag} keepalive not sent`);
22839
+ }, KEEPALIVE_MS);
22840
+ this.keepalive.unref?.();
22841
+ this.opts.logger?.debug?.(`[rtc] ${this.tag} live start sent`);
22842
+ return this;
22843
+ }
22844
+ stop() {
22845
+ if (!this.running)
22846
+ return;
22847
+ this.teardown();
22848
+ const { session, seg, accountId } = this.opts;
22849
+ session.sendCommand(buildPortalPacket({
22850
+ commandId: PORTAL_CMD_SET_PAYLOAD,
22851
+ channel: PORTAL_STATION_CHANNEL,
22852
+ segment: seg.next(),
22853
+ isResponse: LIVE_STREAM_ID,
22854
+ payload: { account_id: accountId, cmd: LIVE_STOP, payload: {} }
22855
+ }));
22856
+ this.emit("stop");
22857
+ }
22858
+ teardown() {
22859
+ this.running = false;
22860
+ if (this.keepalive)
22861
+ clearInterval(this.keepalive);
22862
+ this.keepalive = void 0;
22863
+ const { session } = this.opts;
22864
+ session.off("mediaData", this.onMedia);
22865
+ session.off("commandData", this.onCommand);
22866
+ session.off("close", this.onClose);
22867
+ }
22868
+ fail(err) {
22869
+ if (!this.running)
22870
+ return;
22871
+ this.teardown();
22872
+ this.emit("error", err);
22873
+ }
22874
+ /** The start's ACK: a non-zero result code ends the pull. */
22875
+ onCommandFrame(frame, linkType) {
22876
+ const p = parsePortalPacket(frame, linkType);
22877
+ if (!p?.isResponse || p.segment !== this.startSegment || p.commandId !== PORTAL_CMD_SET_PAYLOAD)
22878
+ return;
22879
+ if (p.errCode !== 0)
22880
+ this.fail(new Error(`rtc live ${this.tag}: start refused (err ${p.errCode})`));
22881
+ }
22882
+ onMediaFrame(frame) {
22883
+ const h = parsePortalHeader(frame);
22884
+ if (!h || h.commandId !== LIVE_MEDIA || h.channel !== LIVE_MEDIA_CHANNEL)
22885
+ return;
22886
+ if (h.paramLength > frame.length - PORTAL_HEADER_LENGTH)
22887
+ return;
22888
+ const media = parseMediaBody(frame.subarray(PORTAL_HEADER_LENGTH, PORTAL_HEADER_LENGTH + h.paramLength));
22889
+ if (!media)
22890
+ return;
22891
+ const { data, width, height } = media;
22892
+ if (width && height) {
22893
+ this.width = width;
22894
+ this.height = height;
22895
+ }
22896
+ const out = {
22897
+ codec: "h265",
22898
+ data,
22899
+ keyframe: hasIdr(data, "h265"),
22900
+ width: this.width,
22901
+ height: this.height
22902
+ };
22903
+ this.emit("video", out);
22904
+ }
22905
+ };
22906
+
22907
+ // dist/transport/rtc/session.js
22908
+ import { EventEmitter as EventEmitter10 } from "node:events";
22909
+
22910
+ // dist/transport/rtc/peer.js
22911
+ import { EventEmitter as EventEmitter8 } from "node:events";
22912
+
22753
22913
  // dist/transport/rtc/ptcs-framer.js
22754
22914
  var MAGIC3 = Buffer.from("PTCS", "ascii");
22755
22915
  var PTCS_HEADER_LENGTH = 28;
@@ -22759,10 +22919,15 @@ var FLAG_LAST = 1024;
22759
22919
  var LENGTH_MASK = 1023;
22760
22920
  var PtcsChannel = {
22761
22921
  COMMAND: 0,
22762
- NOTIFY: 2
22922
+ NOTIFY: 2,
22923
+ LIVE: 5
22763
22924
  };
22764
22925
  function linkTypeForChannel(channel) {
22765
- return channel === PtcsChannel.NOTIFY ? PortalLinkType.NOTIFY : PortalLinkType.COMMAND;
22926
+ if (channel === PtcsChannel.NOTIFY)
22927
+ return PortalLinkType.NOTIFY;
22928
+ if (channel === PtcsChannel.LIVE)
22929
+ return PortalLinkType.LIVE;
22930
+ return PortalLinkType.COMMAND;
22766
22931
  }
22767
22932
  var PTCS_STALE_MS = 15e3;
22768
22933
  function parsePtcsHeader(packet) {
@@ -23028,7 +23193,7 @@ async function loadNativePeerFactory() {
23028
23193
  return (name, config) => new ndc.PeerConnection(name, config);
23029
23194
  }
23030
23195
  var ANSWER_TIMEOUT_MS = 15e3;
23031
- var RtcPeer = class extends EventEmitter7 {
23196
+ var RtcPeer = class extends EventEmitter8 {
23032
23197
  opts;
23033
23198
  pc;
23034
23199
  channels = /* @__PURE__ */ new Map();
@@ -23145,6 +23310,16 @@ var RtcPeer = class extends EventEmitter7 {
23145
23310
  }
23146
23311
  this.addNow(candidate);
23147
23312
  }
23313
+ /**
23314
+ * Send bytes on the command channel as they are, without PTCS framing; false when it isn't open. The
23315
+ * portal's data-channel keepalive rides the wire unframed.
23316
+ */
23317
+ sendRaw(bytes2) {
23318
+ const dc = this.channels.get(COMMAND_CHANNEL);
23319
+ if (!dc?.isOpen())
23320
+ return false;
23321
+ return dc.sendMessageBinary(bytes2);
23322
+ }
23148
23323
  /**
23149
23324
  * Send one portal packet on the command channel, through the framer. False when the channel is not
23150
23325
  * usable or the native send refused a wire packet, so a dropped frame does not read as success.
@@ -23251,7 +23426,7 @@ var RtcPeer = class extends EventEmitter7 {
23251
23426
  };
23252
23427
 
23253
23428
  // dist/transport/rtc/signaling.js
23254
- import { EventEmitter as EventEmitter8 } from "node:events";
23429
+ import { EventEmitter as EventEmitter9 } from "node:events";
23255
23430
  import { createHmac as createHmac3, randomUUID as randomUUID2 } from "node:crypto";
23256
23431
  var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
23257
23432
  var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
@@ -23267,7 +23442,7 @@ var defaultSocket = (url, init) => new WebSocket(url, {
23267
23442
  protocols: init.protocols,
23268
23443
  headers: { Origin: init.origin }
23269
23444
  });
23270
- var RtcSignalingClient = class extends EventEmitter8 {
23445
+ var RtcSignalingClient = class extends EventEmitter9 {
23271
23446
  opts;
23272
23447
  ws;
23273
23448
  sign;
@@ -23492,7 +23667,7 @@ var RtcSignalingClient = class extends EventEmitter8 {
23492
23667
 
23493
23668
  // dist/transport/rtc/session.js
23494
23669
  var AUTH_TIMEOUT_MS = 15e3;
23495
- var RtcSession = class extends EventEmitter9 {
23670
+ var RtcSession = class extends EventEmitter10 {
23496
23671
  opts;
23497
23672
  signaling;
23498
23673
  peer;
@@ -23544,7 +23719,12 @@ var RtcSession = class extends EventEmitter9 {
23544
23719
  }
23545
23720
  });
23546
23721
  this.peer.on("error", (e) => this.emit("error", e));
23547
- this.peer.on("data", (frame, linkType) => this.emit("commandData", frame, linkType));
23722
+ this.peer.on("data", (frame, linkType) => {
23723
+ if (linkType === PortalLinkType.LIVE)
23724
+ this.emit("mediaData", frame);
23725
+ else
23726
+ this.emit("commandData", frame, linkType);
23727
+ });
23548
23728
  }
23549
23729
  get isConnected() {
23550
23730
  return this.connected;
@@ -23569,6 +23749,10 @@ var RtcSession = class extends EventEmitter9 {
23569
23749
  sendCommand(portalPacket) {
23570
23750
  return this.peer.sendCommand(portalPacket);
23571
23751
  }
23752
+ /** Send unframed bytes on the command channel; false when it isn't open. */
23753
+ sendRaw(bytes2) {
23754
+ return this.peer.sendRaw(bytes2);
23755
+ }
23572
23756
  /**
23573
23757
  * Hang up and tear both sides down; `close` fires once, here if nothing announced it before. A
23574
23758
  * `connect()` still waiting for the signalling auth is rejected and its timer stopped.
@@ -23715,16 +23899,21 @@ var RtcSession = class extends EventEmitter9 {
23715
23899
  };
23716
23900
 
23717
23901
  // dist/transport/rtc/command-router.js
23718
- var PORTAL_CMD_SET_PAYLOAD = 1350;
23719
23902
  var PORTAL_CMD_CONTROL_PAYLOAD = 1700;
23720
23903
  var PORTAL_CMD_NOTIFY_PAYLOAD = 1351;
23721
- var PORTAL_STATION_CHANNEL = 255;
23722
23904
  var ACK_TIMEOUT_MS = 8e3;
23723
23905
  var CONNECT_TIMEOUT_MS2 = 12e3;
23724
23906
  var IDLE_CLOSE_MS = 6e4;
23907
+ var StationStreamBusyError = class extends Error {
23908
+ constructor(stationSn, servingSn) {
23909
+ super(`rtc live: ${stationSn} is serving ${servingSn}; the station streams one camera at a time`);
23910
+ this.name = "StationStreamBusyError";
23911
+ }
23912
+ };
23725
23913
  var RtcCommandRouter = class {
23726
23914
  deps;
23727
23915
  sessions = /* @__PURE__ */ new Map();
23916
+ lives = /* @__PURE__ */ new Map();
23728
23917
  constructor(deps) {
23729
23918
  this.deps = deps;
23730
23919
  }
@@ -23748,11 +23937,112 @@ var RtcCommandRouter = class {
23748
23937
  st.queue = run.catch(() => void 0);
23749
23938
  return run;
23750
23939
  }
23751
- /** Tear down every station session (logout / shutdown). */
23940
+ /**
23941
+ * The media of a camera on a T9000: live video, a readable feed and a live still over the station's
23942
+ * session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
23943
+ */
23944
+ mediaProviderFor(route) {
23945
+ return {
23946
+ live: (opts) => this.withLiveSource(route, opts ?? {}, (source) => attachUnlessAborted(source, opts?.signal)),
23947
+ openReadable: (opts) => this.withLiveSource(route, opts ?? {}, (source) => openReadableFromConsumer(attachUnlessAborted(source, opts?.signal), opts)),
23948
+ snapshotLive: (opts) => this.withLiveSource(route, opts ?? {}, (source) => captureSnapshotFromShared(source, { ...opts, logger: this.deps.logger, ffmpegPath: this.deps.ffmpegPath })).catch((e) => {
23949
+ if (!(e instanceof StationStreamBusyError))
23950
+ throw e;
23951
+ throw new LiveSnapshotUnavailableError("source-failed", e.message, { cause: e });
23952
+ }),
23953
+ record: async () => {
23954
+ throw new Error(`record is not available for ${route.cameraSn} over the T9000 control channel`);
23955
+ }
23956
+ };
23957
+ }
23958
+ /**
23959
+ * Tear down every station session (logout / shutdown). Each live source is disposed before its session
23960
+ * closes, so its consumers end cleanly and its stop still rides the session.
23961
+ */
23752
23962
  close() {
23963
+ for (const [sn, live] of this.lives) {
23964
+ this.lives.delete(sn);
23965
+ live.source.dispose();
23966
+ }
23753
23967
  for (const [sn, st] of this.sessions)
23754
23968
  this.drop(sn, st);
23755
23969
  }
23970
+ /**
23971
+ * Hand `use` the station's live source for this camera, in the same turn the source is created or reused,
23972
+ * so a consumer it attaches is counted before any other caller looks.
23973
+ *
23974
+ * The station streams one camera at a time. Another camera whose source has consumers refuses this one,
23975
+ * before the session is awaited and again after; one with none (never attached or lingering)
23976
+ * holds nothing a viewer needs, so it is disposed and the slot taken. The first caller's
23977
+ * {@link SharedSourceHints} configure a new source.
23978
+ */
23979
+ async withLiveSource(route, opts, use) {
23980
+ opts.signal?.throwIfAborted();
23981
+ this.refuseIfWatched(route);
23982
+ const identity = this.deps.identity();
23983
+ if (!identity)
23984
+ throw new Error(`rtc: not logged in, cannot open ${route.stationSn}`);
23985
+ const accountId = route.adminUserId || identity.userId;
23986
+ const st = await abortable(this.stationSession(route, accountId, identity), opts.signal);
23987
+ this.refuseIfWatched(route);
23988
+ const current = this.lives.get(route.stationSn);
23989
+ if (current && current.cameraSn !== route.cameraSn) {
23990
+ this.deps.logger?.debug?.(`[rtc] ${route.stationSn} releasing the unwatched source of ${current.cameraSn} for ${route.cameraSn}`);
23991
+ this.lives.delete(route.stationSn);
23992
+ current.source.dispose();
23993
+ }
23994
+ let live = this.lives.get(route.stationSn);
23995
+ if (!live) {
23996
+ live = { cameraSn: route.cameraSn, source: this.newLiveSource(route, st, accountId, opts) };
23997
+ this.lives.set(route.stationSn, live);
23998
+ }
23999
+ return await use(live.source);
24000
+ }
24001
+ /** Refuse a camera while another camera on its station has consumers. */
24002
+ refuseIfWatched(route) {
24003
+ const current = this.lives.get(route.stationSn);
24004
+ if (current && current.cameraSn !== route.cameraSn && current.source.consumerCount > 0) {
24005
+ throw new StationStreamBusyError(route.stationSn, current.cameraSn);
24006
+ }
24007
+ }
24008
+ newLiveSource(route, st, accountId, hints) {
24009
+ let leased = false;
24010
+ const release = () => {
24011
+ if (!leased)
24012
+ return;
24013
+ leased = false;
24014
+ st.leases = Math.max(0, st.leases - 1);
24015
+ this.touch(route.stationSn, st);
24016
+ };
24017
+ const source = new SharedLiveSource({
24018
+ makeStream: () => new RtcLiveStream({
24019
+ session: st.session,
24020
+ seg: st.seg,
24021
+ stationSn: route.stationSn,
24022
+ channel: route.channel,
24023
+ accountId,
24024
+ logger: this.deps.logger
24025
+ }),
24026
+ label: `${route.stationSn}#${route.channel}`,
24027
+ logger: this.deps.logger,
24028
+ powered: hints.powered,
24029
+ preBufferSeconds: hints.preBufferSeconds,
24030
+ onActive: () => {
24031
+ if (leased)
24032
+ return;
24033
+ leased = true;
24034
+ st.leases++;
24035
+ if (st.idle)
24036
+ clearTimeout(st.idle);
24037
+ },
24038
+ onStopped: () => {
24039
+ release();
24040
+ if (this.lives.get(route.stationSn)?.source === source)
24041
+ this.lives.delete(route.stationSn);
24042
+ }
24043
+ });
24044
+ return source;
24045
+ }
23756
24046
  /**
23757
24047
  * Completes on the command ACK or a correlated control-result notification. The notification must
23758
24048
  * match the request channel, parameter and segment and contain a structured payload. Receipt does
@@ -23822,7 +24112,7 @@ var RtcCommandRouter = class {
23822
24112
  peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
23823
24113
  });
23824
24114
  const ready = this.bringUp(sn, session);
23825
- const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve() };
24115
+ const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
23826
24116
  session.on("error", (e) => this.deps.onError?.(e));
23827
24117
  session.on("close", () => {
23828
24118
  if (this.sessions.get(sn) === st)
@@ -23876,6 +24166,8 @@ var RtcCommandRouter = class {
23876
24166
  touch(sn, st) {
23877
24167
  if (st.idle)
23878
24168
  clearTimeout(st.idle);
24169
+ if (st.leases > 0)
24170
+ return;
23879
24171
  st.idle = setTimeout(() => this.drop(sn, st), IDLE_CLOSE_MS);
23880
24172
  st.idle.unref?.();
23881
24173
  }
@@ -25505,7 +25797,7 @@ async function resolveLightEffect(mega, lightId) {
25505
25797
  }
25506
25798
 
25507
25799
  // dist/transport/push/push-client.js
25508
- import { EventEmitter as EventEmitter11 } from "node:events";
25800
+ import { EventEmitter as EventEmitter12 } from "node:events";
25509
25801
  import tls2 from "node:tls";
25510
25802
 
25511
25803
  // dist/transport/push/proto.js
@@ -25760,7 +26052,7 @@ var MessageTag;
25760
26052
  })(MessageTag || (MessageTag = {}));
25761
26053
 
25762
26054
  // dist/transport/push/parser.js
25763
- import { EventEmitter as EventEmitter10 } from "node:events";
26055
+ import { EventEmitter as EventEmitter11 } from "node:events";
25764
26056
  var State;
25765
26057
  (function(State2) {
25766
26058
  State2[State2["VERSION_TAG_AND_SIZE"] = 0] = "VERSION_TAG_AND_SIZE";
@@ -25778,7 +26070,7 @@ var TAG_TO_TYPE = {
25778
26070
  [MessageTag.DataMessageStanza]: "mcs_proto.DataMessageStanza",
25779
26071
  [MessageTag.StreamErrorStanza]: "mcs_proto.StreamErrorStanza"
25780
26072
  };
25781
- var McsParser = class extends EventEmitter10 {
26073
+ var McsParser = class extends EventEmitter11 {
25782
26074
  data = Buffer.alloc(0);
25783
26075
  state = State.VERSION_TAG_AND_SIZE;
25784
26076
  messageTag = 0;
@@ -25956,7 +26248,7 @@ function normalizePushEvent(raw) {
25956
26248
  raw
25957
26249
  };
25958
26250
  }
25959
- var PushClient = class _PushClient extends EventEmitter11 {
26251
+ var PushClient = class _PushClient extends EventEmitter12 {
25960
26252
  creds;
25961
26253
  logger;
25962
26254
  /** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
@@ -27143,6 +27435,8 @@ function tuyaDevIdFrom(raw) {
27143
27435
  var DEFAULT_POLL_MS = 6e5;
27144
27436
  var DEFAULT_PREWARM_TIERS = ["wired", "battery"];
27145
27437
  var SESSION_RECYCLE_WAIT_MS = 5e3;
27438
+ var PUSH_RETRY_MIN_MS = 3e4;
27439
+ var PUSH_RETRY_MAX_MS = 6e5;
27146
27440
  var RealtimeStartupSupersededError = class extends Error {
27147
27441
  };
27148
27442
  var RefreshWindowClosedError = class extends Error {
@@ -27158,7 +27452,7 @@ function readinessSnapshot(readiness, state = readiness.state) {
27158
27452
  wiredP2p: Object.freeze({ ...readiness.wiredP2p })
27159
27453
  });
27160
27454
  }
27161
- var EufyMega = class extends EventEmitter12 {
27455
+ var EufyMega = class extends EventEmitter13 {
27162
27456
  opts;
27163
27457
  mega;
27164
27458
  /**
@@ -27241,6 +27535,8 @@ var EufyMega = class extends EventEmitter12 {
27241
27535
  mapStores = /* @__PURE__ */ new Map();
27242
27536
  /** Re-armed after each cloud-param poll; cancelled by {@link disconnect}. */
27243
27537
  pollTimer = new Timer();
27538
+ /** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
27539
+ pushRetryTimer = new Timer();
27244
27540
  constructor(opts) {
27245
27541
  super();
27246
27542
  this.opts = opts;
@@ -27285,6 +27581,7 @@ var EufyMega = class extends EventEmitter12 {
27285
27581
  shard: () => this.mega.rtcShard,
27286
27582
  country: opts.countryCode,
27287
27583
  logger: opts.logger,
27584
+ ffmpegPath: opts.ffmpegPath,
27288
27585
  onError: (e) => this.reportError(e)
27289
27586
  });
27290
27587
  this.mqtt = new MqttCommandRouter({
@@ -27996,7 +28293,7 @@ var EufyMega = class extends EventEmitter12 {
27996
28293
  * current. With nothing retained the refusal stands.
27997
28294
  */
27998
28295
  mediaProviderFor(sn) {
27999
- const media = this.p2p.mediaProviderFor(sn);
28296
+ const media = this.rtcMediaFor(sn) ?? this.p2p.mediaProviderFor(sn);
28000
28297
  const cache = this.storedImages;
28001
28298
  if (!cache)
28002
28299
  return media;
@@ -28056,24 +28353,48 @@ var EufyMega = class extends EventEmitter12 {
28056
28353
  return this.tuya.dispatchCommand(sn, cmd);
28057
28354
  return this.mqtt.dispatchCommand(sn, cmd);
28058
28355
  }
28356
+ const route = this.rtcRouteFor(sn);
28357
+ if (route) {
28358
+ if (route.attached && this.registry.serialForFrame(route.stationSn, cmd.channel) !== sn)
28359
+ return Promise.reject(new Error("RTC command requires an unambiguous attached-device channel"));
28360
+ return this.rtc.dispatchCommand(route, cmd);
28361
+ }
28362
+ return this.p2p.dispatchCommand(sn, cmd);
28363
+ }
28364
+ /**
28365
+ * The RTC route for a T9000 or T8N00 station or a device attached to one, decided by the station's model
28366
+ * classification; `undefined` for any other device. A T8N00 route carries its native `call` signalling. The admin id is the station's
28367
+ * `member.admin_user_id`, else the logged-in user.
28368
+ */
28369
+ rtcRouteFor(sn) {
28059
28370
  const devices = this.registry.list();
28060
28371
  const target = devices.find((d) => d.sn === sn);
28061
28372
  const stationSn = target?.stationSn || sn;
28062
28373
  const station = devices.find((d) => d.sn === stationSn);
28063
- const stationRaw = station?.raw ?? {};
28064
- const deviceType = typeof stationRaw.device_type === "number" ? stationRaw.device_type : void 0;
28374
+ const raw = station?.raw ?? {};
28375
+ const deviceType = typeof raw.device_type === "number" ? raw.device_type : void 0;
28065
28376
  const family = { deviceType, model: station?.model };
28066
28377
  const native = isNvrT8N00(family);
28067
- if (target && station && (isStation9000(family) || native)) {
28068
- const attached = stationSn !== sn;
28069
- if (attached && this.registry.serialForFrame(stationSn, cmd.channel) !== sn)
28070
- return Promise.reject(new Error("RTC command requires an unambiguous attached-device channel"));
28071
- const member = stationRaw.member?.admin_user_id;
28072
- const adminUserId = typeof member === "string" && member ? member : void 0;
28073
- const base = { stationSn, adminUserId, attached };
28074
- return this.rtc.dispatchCommand(native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base, cmd);
28075
- }
28076
- return this.p2p.dispatchCommand(sn, cmd);
28378
+ if (!target || !station || !(isStation9000(family) || native))
28379
+ return void 0;
28380
+ const member = raw.member?.admin_user_id;
28381
+ const adminUserId = typeof member === "string" && member ? member : void 0;
28382
+ const base = { stationSn, adminUserId, attached: stationSn !== sn };
28383
+ return native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base;
28384
+ }
28385
+ /**
28386
+ * The RTC media provider for a camera attached to a T9000 whose `device_channel` resolves back to it on
28387
+ * the station; `undefined` otherwise, including a camera behind a T8N00, whose route carries commands only.
28388
+ */
28389
+ rtcMediaFor(sn) {
28390
+ const route = this.rtcRouteFor(sn);
28391
+ if (!route?.attached || route.signalingMode === "call")
28392
+ return void 0;
28393
+ const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
28394
+ const channel = typeof raw.device_channel === "number" ? raw.device_channel : void 0;
28395
+ if (channel === void 0 || this.registry.serialForFrame(route.stationSn, channel) !== sn)
28396
+ return void 0;
28397
+ return this.rtc.mediaProviderFor({ ...route, cameraSn: sn, channel });
28077
28398
  }
28078
28399
  /**
28079
28400
  * List + classify devices across all houses (mega API). Each device is tagged with its API backend
@@ -28307,13 +28628,38 @@ var EufyMega = class extends EventEmitter12 {
28307
28628
  this.schedulePoll();
28308
28629
  const failures = readiness.push.failed + readiness.mqtt.failed + readiness.wiredP2p.failed;
28309
28630
  this.settleRealtimeGeneration(generation, failures || startupFailed ? "partial" : "ready");
28631
+ if (readiness.push.failed)
28632
+ this.retryPush(generation);
28310
28633
  } catch (e) {
28311
28634
  this.reportError(e);
28312
28635
  readiness.push.failed += readiness.push.pending;
28313
28636
  readiness.push.pending = 0;
28314
28637
  this.settleRealtimeGeneration(generation, epoch === this.realtimeEpoch ? "partial" : "superseded");
28638
+ if (epoch === this.realtimeEpoch)
28639
+ this.retryPush(generation);
28315
28640
  }
28316
28641
  }
28642
+ /**
28643
+ * Bring push up again after the generation's startup attempt failed, backing off between attempts
28644
+ * until one connects, the generation is superseded, or it is aborted by {@link disconnect}.
28645
+ *
28646
+ * The MCS socket reconnects on its own only once it has connected. The settled
28647
+ * {@link RealtimeReadiness} is a startup snapshot and is not revised when a retry connects.
28648
+ */
28649
+ retryPush(generation, delayMs = PUSH_RETRY_MIN_MS) {
28650
+ this.pushRetryTimer.arm(delayMs, () => {
28651
+ this.startPush(generation.abort.signal).then((client) => {
28652
+ if (generation.epoch !== this.realtimeEpoch)
28653
+ return client.close();
28654
+ this.pushClient = client;
28655
+ }, (error) => {
28656
+ if (generation.epoch !== this.realtimeEpoch)
28657
+ return;
28658
+ this.reportError(error);
28659
+ this.retryPush(generation, Math.min(delayMs * 2, PUSH_RETRY_MAX_MS));
28660
+ });
28661
+ });
28662
+ }
28317
28663
  /** Resolve a generation with an immutable count snapshot; later transport completions are ignored. */
28318
28664
  settleRealtimeGeneration(generation, state) {
28319
28665
  if (generation.settled)
@@ -29169,6 +29515,7 @@ var EufyMega = class extends EventEmitter12 {
29169
29515
  */
29170
29516
  async teardownRealtime() {
29171
29517
  this.pollTimer.cancel();
29518
+ this.pushRetryTimer.cancel();
29172
29519
  this.lastStateAnnounced.clear();
29173
29520
  await this.closeMqttTransports();
29174
29521
  this.rtc.close();
@@ -29676,7 +30023,7 @@ var SolixClient = class _SolixClient {
29676
30023
  };
29677
30024
 
29678
30025
  // dist/transport/mqtt/solix-mqtt.js
29679
- import { EventEmitter as EventEmitter13 } from "node:events";
30026
+ import { EventEmitter as EventEmitter14 } from "node:events";
29680
30027
  var SOLIX_METER_FIELD_NAMES = {
29681
30028
  168: "meterPowerL1",
29682
30029
  // Anker Modbus: primary_phase_1_active_power
@@ -29854,7 +30201,7 @@ function addSolarbankScalars(frame, out) {
29854
30201
  out.gridExportLimit = df.readUInt16LE(5);
29855
30202
  }
29856
30203
  }
29857
- var SolixMqtt = class extends EventEmitter13 {
30204
+ var SolixMqtt = class extends EventEmitter14 {
29858
30205
  transport;
29859
30206
  appName;
29860
30207
  userId;