@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.
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
 
@@ -17751,7 +17764,13 @@ function beginsAccessUnit(body) {
17751
17764
  return body[2] === 1 || body[2] === 0 && body[3] === 1;
17752
17765
  }
17753
17766
  function unitOf(header, data) {
17754
- return { keyframe: header.keyframe, width: header.width, height: header.height, data };
17767
+ return {
17768
+ sourceTimestamp: header.timestamp,
17769
+ keyframe: header.keyframe,
17770
+ width: header.width,
17771
+ height: header.height,
17772
+ data
17773
+ };
17755
17774
  }
17756
17775
 
17757
17776
  // dist/transport/p2p/annexb.js
@@ -17854,6 +17873,18 @@ function startCodeBegin(buf, payloadOffset) {
17854
17873
  return threeByte - 1;
17855
17874
  return threeByte;
17856
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
+ }
17857
17888
  var MAX_CODED_DIMENSION = 32768;
17858
17889
  var H264_CHROMA_PROFILES = /* @__PURE__ */ new Set([100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135, 144]);
17859
17890
  var CHROMA_SUBSAMPLING = {
@@ -18280,6 +18311,7 @@ var LiveStream = class extends EventEmitter3 {
18280
18311
  this.lastCodec = sniffAnnexbCodec(unit.data) ?? this.lastCodec;
18281
18312
  this.settleKeepalive();
18282
18313
  this.emit("video", {
18314
+ sourceTimestamp: unit.sourceTimestamp,
18283
18315
  keyframe: unit.keyframe,
18284
18316
  width: unit.width,
18285
18317
  height: unit.height,
@@ -19623,6 +19655,14 @@ var SharedLiveSource = class {
19623
19655
  this.opts.onIdle?.();
19624
19656
  }
19625
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
+ }
19626
19666
 
19627
19667
  // dist/transport/p2p/session-manager.js
19628
19668
  var SessionSupersededError = class extends Error {
@@ -20979,17 +21019,6 @@ function stationChannels(devices) {
20979
21019
 
20980
21020
  // dist/transport/p2p/command-router.js
20981
21021
  var DIRECT_CMD_SENDS = 5;
20982
- function abortable(work, signal) {
20983
- if (!signal)
20984
- return work;
20985
- signal.throwIfAborted();
20986
- return Promise.race([
20987
- work,
20988
- new Promise((_resolve, reject) => {
20989
- signal.addEventListener("abort", () => reject(signal.reason), { once: true });
20990
- })
20991
- ]);
20992
- }
20993
21022
  var LEVEL2_GRACE_MS = 25e3;
20994
21023
  var LEVEL2_SETTLE_MS = 8e3;
20995
21024
  var P2P_STATION_WAITS = {
@@ -21481,11 +21510,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21481
21510
  },
21482
21511
  live: async (opts) => {
21483
21512
  const source = await this.sharedLiveSourceFor(sn, opts, true);
21484
- return this.attachUnlessAborted(source, opts?.signal);
21513
+ return attachUnlessAborted(source, opts?.signal);
21485
21514
  },
21486
21515
  openReadable: async (opts) => {
21487
21516
  const source = await this.sharedLiveSourceFor(sn, opts ?? {}, true);
21488
- return openReadableFromConsumer(this.attachUnlessAborted(source, opts?.signal), opts);
21517
+ return openReadableFromConsumer(attachUnlessAborted(source, opts?.signal), opts);
21489
21518
  },
21490
21519
  recordFragments: (opts) => this.recordFragments(sn, opts),
21491
21520
  talkback: (opts) => this.openTalkback(sn, opts),
@@ -21853,22 +21882,6 @@ var P2PCommandRouter = class _P2PCommandRouter {
21853
21882
  * A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
21854
21883
  * unaffected.
21855
21884
  */
21856
- /**
21857
- * Attach a consumer, unless the caller has already abandoned the call.
21858
- *
21859
- * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
21860
- * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
21861
- * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
21862
- * not.
21863
- */
21864
- attachUnlessAborted(source, signal) {
21865
- const consumer = source.attach();
21866
- if (signal?.aborted) {
21867
- consumer.detach();
21868
- signal.throwIfAborted();
21869
- }
21870
- return consumer;
21871
- }
21872
21885
  /**
21873
21886
  * Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
21874
21887
  * on a session goes with it, and a media session opened for this source alone is closed: nothing else
@@ -22638,20 +22651,21 @@ var P2PCommandRouter = class _P2PCommandRouter {
22638
22651
  }
22639
22652
  };
22640
22653
 
22641
- // dist/transport/rtc/session.js
22642
- import { EventEmitter as EventEmitter9 } from "node:events";
22643
-
22644
- // dist/transport/rtc/peer.js
22654
+ // dist/transport/rtc/live.js
22645
22655
  import { EventEmitter as EventEmitter7 } from "node:events";
22646
22656
 
22647
22657
  // dist/transport/rtc/portal-packet.js
22648
22658
  var MAGIC2 = Buffer.from("XZYH", "ascii");
22649
22659
  var PORTAL_HEADER_LENGTH = 16;
22660
+ var PORTAL_CMD_SET_PAYLOAD = 1350;
22661
+ var PORTAL_STATION_CHANNEL = 255;
22650
22662
  var PortalLinkType = {
22651
22663
  /** Command channel: requests out, acknowledgements back. */
22652
22664
  COMMAND: 1,
22653
22665
  /** Station-originated frames: pushes, notify payloads, control results. */
22654
- NOTIFY: 3
22666
+ NOTIFY: 3,
22667
+ /** Live video. */
22668
+ LIVE: 5
22655
22669
  };
22656
22670
  function encodeLe(value, width) {
22657
22671
  const buf = Buffer.alloc(width);
@@ -22743,6 +22757,159 @@ var SegmentCounter = class {
22743
22757
  }
22744
22758
  };
22745
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
+
22746
22913
  // dist/transport/rtc/ptcs-framer.js
22747
22914
  var MAGIC3 = Buffer.from("PTCS", "ascii");
22748
22915
  var PTCS_HEADER_LENGTH = 28;
@@ -22752,10 +22919,15 @@ var FLAG_LAST = 1024;
22752
22919
  var LENGTH_MASK = 1023;
22753
22920
  var PtcsChannel = {
22754
22921
  COMMAND: 0,
22755
- NOTIFY: 2
22922
+ NOTIFY: 2,
22923
+ LIVE: 5
22756
22924
  };
22757
22925
  function linkTypeForChannel(channel) {
22758
- 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;
22759
22931
  }
22760
22932
  var PTCS_STALE_MS = 15e3;
22761
22933
  function parsePtcsHeader(packet) {
@@ -23021,7 +23193,7 @@ async function loadNativePeerFactory() {
23021
23193
  return (name, config) => new ndc.PeerConnection(name, config);
23022
23194
  }
23023
23195
  var ANSWER_TIMEOUT_MS = 15e3;
23024
- var RtcPeer = class extends EventEmitter7 {
23196
+ var RtcPeer = class extends EventEmitter8 {
23025
23197
  opts;
23026
23198
  pc;
23027
23199
  channels = /* @__PURE__ */ new Map();
@@ -23138,6 +23310,16 @@ var RtcPeer = class extends EventEmitter7 {
23138
23310
  }
23139
23311
  this.addNow(candidate);
23140
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
+ }
23141
23323
  /**
23142
23324
  * Send one portal packet on the command channel, through the framer. False when the channel is not
23143
23325
  * usable or the native send refused a wire packet, so a dropped frame does not read as success.
@@ -23244,7 +23426,7 @@ var RtcPeer = class extends EventEmitter7 {
23244
23426
  };
23245
23427
 
23246
23428
  // dist/transport/rtc/signaling.js
23247
- import { EventEmitter as EventEmitter8 } from "node:events";
23429
+ import { EventEmitter as EventEmitter9 } from "node:events";
23248
23430
  import { createHmac as createHmac3, randomUUID as randomUUID2 } from "node:crypto";
23249
23431
  var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
23250
23432
  var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
@@ -23260,7 +23442,7 @@ var defaultSocket = (url, init) => new WebSocket(url, {
23260
23442
  protocols: init.protocols,
23261
23443
  headers: { Origin: init.origin }
23262
23444
  });
23263
- var RtcSignalingClient = class extends EventEmitter8 {
23445
+ var RtcSignalingClient = class extends EventEmitter9 {
23264
23446
  opts;
23265
23447
  ws;
23266
23448
  sign;
@@ -23485,7 +23667,7 @@ var RtcSignalingClient = class extends EventEmitter8 {
23485
23667
 
23486
23668
  // dist/transport/rtc/session.js
23487
23669
  var AUTH_TIMEOUT_MS = 15e3;
23488
- var RtcSession = class extends EventEmitter9 {
23670
+ var RtcSession = class extends EventEmitter10 {
23489
23671
  opts;
23490
23672
  signaling;
23491
23673
  peer;
@@ -23537,7 +23719,12 @@ var RtcSession = class extends EventEmitter9 {
23537
23719
  }
23538
23720
  });
23539
23721
  this.peer.on("error", (e) => this.emit("error", e));
23540
- 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
+ });
23541
23728
  }
23542
23729
  get isConnected() {
23543
23730
  return this.connected;
@@ -23562,6 +23749,10 @@ var RtcSession = class extends EventEmitter9 {
23562
23749
  sendCommand(portalPacket) {
23563
23750
  return this.peer.sendCommand(portalPacket);
23564
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
+ }
23565
23756
  /**
23566
23757
  * Hang up and tear both sides down; `close` fires once, here if nothing announced it before. A
23567
23758
  * `connect()` still waiting for the signalling auth is rejected and its timer stopped.
@@ -23708,16 +23899,21 @@ var RtcSession = class extends EventEmitter9 {
23708
23899
  };
23709
23900
 
23710
23901
  // dist/transport/rtc/command-router.js
23711
- var PORTAL_CMD_SET_PAYLOAD = 1350;
23712
23902
  var PORTAL_CMD_CONTROL_PAYLOAD = 1700;
23713
23903
  var PORTAL_CMD_NOTIFY_PAYLOAD = 1351;
23714
- var PORTAL_STATION_CHANNEL = 255;
23715
23904
  var ACK_TIMEOUT_MS = 8e3;
23716
23905
  var CONNECT_TIMEOUT_MS2 = 12e3;
23717
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
+ };
23718
23913
  var RtcCommandRouter = class {
23719
23914
  deps;
23720
23915
  sessions = /* @__PURE__ */ new Map();
23916
+ lives = /* @__PURE__ */ new Map();
23721
23917
  constructor(deps) {
23722
23918
  this.deps = deps;
23723
23919
  }
@@ -23741,11 +23937,112 @@ var RtcCommandRouter = class {
23741
23937
  st.queue = run.catch(() => void 0);
23742
23938
  return run;
23743
23939
  }
23744
- /** 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
+ */
23745
23962
  close() {
23963
+ for (const [sn, live] of this.lives) {
23964
+ this.lives.delete(sn);
23965
+ live.source.dispose();
23966
+ }
23746
23967
  for (const [sn, st] of this.sessions)
23747
23968
  this.drop(sn, st);
23748
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
+ }
23749
24046
  /**
23750
24047
  * Completes on the command ACK or a correlated control-result notification. The notification must
23751
24048
  * match the request channel, parameter and segment and contain a structured payload. Receipt does
@@ -23815,7 +24112,7 @@ var RtcCommandRouter = class {
23815
24112
  peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
23816
24113
  });
23817
24114
  const ready = this.bringUp(sn, session);
23818
- const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve() };
24115
+ const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
23819
24116
  session.on("error", (e) => this.deps.onError?.(e));
23820
24117
  session.on("close", () => {
23821
24118
  if (this.sessions.get(sn) === st)
@@ -23869,6 +24166,8 @@ var RtcCommandRouter = class {
23869
24166
  touch(sn, st) {
23870
24167
  if (st.idle)
23871
24168
  clearTimeout(st.idle);
24169
+ if (st.leases > 0)
24170
+ return;
23872
24171
  st.idle = setTimeout(() => this.drop(sn, st), IDLE_CLOSE_MS);
23873
24172
  st.idle.unref?.();
23874
24173
  }
@@ -25498,7 +25797,7 @@ async function resolveLightEffect(mega, lightId) {
25498
25797
  }
25499
25798
 
25500
25799
  // dist/transport/push/push-client.js
25501
- import { EventEmitter as EventEmitter11 } from "node:events";
25800
+ import { EventEmitter as EventEmitter12 } from "node:events";
25502
25801
  import tls2 from "node:tls";
25503
25802
 
25504
25803
  // dist/transport/push/proto.js
@@ -25753,7 +26052,7 @@ var MessageTag;
25753
26052
  })(MessageTag || (MessageTag = {}));
25754
26053
 
25755
26054
  // dist/transport/push/parser.js
25756
- import { EventEmitter as EventEmitter10 } from "node:events";
26055
+ import { EventEmitter as EventEmitter11 } from "node:events";
25757
26056
  var State;
25758
26057
  (function(State2) {
25759
26058
  State2[State2["VERSION_TAG_AND_SIZE"] = 0] = "VERSION_TAG_AND_SIZE";
@@ -25771,7 +26070,7 @@ var TAG_TO_TYPE = {
25771
26070
  [MessageTag.DataMessageStanza]: "mcs_proto.DataMessageStanza",
25772
26071
  [MessageTag.StreamErrorStanza]: "mcs_proto.StreamErrorStanza"
25773
26072
  };
25774
- var McsParser = class extends EventEmitter10 {
26073
+ var McsParser = class extends EventEmitter11 {
25775
26074
  data = Buffer.alloc(0);
25776
26075
  state = State.VERSION_TAG_AND_SIZE;
25777
26076
  messageTag = 0;
@@ -25949,7 +26248,7 @@ function normalizePushEvent(raw) {
25949
26248
  raw
25950
26249
  };
25951
26250
  }
25952
- var PushClient = class _PushClient extends EventEmitter11 {
26251
+ var PushClient = class _PushClient extends EventEmitter12 {
25953
26252
  creds;
25954
26253
  logger;
25955
26254
  /** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
@@ -27136,6 +27435,8 @@ function tuyaDevIdFrom(raw) {
27136
27435
  var DEFAULT_POLL_MS = 6e5;
27137
27436
  var DEFAULT_PREWARM_TIERS = ["wired", "battery"];
27138
27437
  var SESSION_RECYCLE_WAIT_MS = 5e3;
27438
+ var PUSH_RETRY_MIN_MS = 3e4;
27439
+ var PUSH_RETRY_MAX_MS = 6e5;
27139
27440
  var RealtimeStartupSupersededError = class extends Error {
27140
27441
  };
27141
27442
  var RefreshWindowClosedError = class extends Error {
@@ -27151,7 +27452,7 @@ function readinessSnapshot(readiness, state = readiness.state) {
27151
27452
  wiredP2p: Object.freeze({ ...readiness.wiredP2p })
27152
27453
  });
27153
27454
  }
27154
- var EufyMega = class extends EventEmitter12 {
27455
+ var EufyMega = class extends EventEmitter13 {
27155
27456
  opts;
27156
27457
  mega;
27157
27458
  /**
@@ -27234,6 +27535,8 @@ var EufyMega = class extends EventEmitter12 {
27234
27535
  mapStores = /* @__PURE__ */ new Map();
27235
27536
  /** Re-armed after each cloud-param poll; cancelled by {@link disconnect}. */
27236
27537
  pollTimer = new Timer();
27538
+ /** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
27539
+ pushRetryTimer = new Timer();
27237
27540
  constructor(opts) {
27238
27541
  super();
27239
27542
  this.opts = opts;
@@ -27278,6 +27581,7 @@ var EufyMega = class extends EventEmitter12 {
27278
27581
  shard: () => this.mega.rtcShard,
27279
27582
  country: opts.countryCode,
27280
27583
  logger: opts.logger,
27584
+ ffmpegPath: opts.ffmpegPath,
27281
27585
  onError: (e) => this.reportError(e)
27282
27586
  });
27283
27587
  this.mqtt = new MqttCommandRouter({
@@ -27989,7 +28293,7 @@ var EufyMega = class extends EventEmitter12 {
27989
28293
  * current. With nothing retained the refusal stands.
27990
28294
  */
27991
28295
  mediaProviderFor(sn) {
27992
- const media = this.p2p.mediaProviderFor(sn);
28296
+ const media = this.rtcMediaFor(sn) ?? this.p2p.mediaProviderFor(sn);
27993
28297
  const cache = this.storedImages;
27994
28298
  if (!cache)
27995
28299
  return media;
@@ -28049,24 +28353,48 @@ var EufyMega = class extends EventEmitter12 {
28049
28353
  return this.tuya.dispatchCommand(sn, cmd);
28050
28354
  return this.mqtt.dispatchCommand(sn, cmd);
28051
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) {
28052
28370
  const devices = this.registry.list();
28053
28371
  const target = devices.find((d) => d.sn === sn);
28054
28372
  const stationSn = target?.stationSn || sn;
28055
28373
  const station = devices.find((d) => d.sn === stationSn);
28056
- const stationRaw = station?.raw ?? {};
28057
- 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;
28058
28376
  const family = { deviceType, model: station?.model };
28059
28377
  const native = isNvrT8N00(family);
28060
- if (target && station && (isStation9000(family) || native)) {
28061
- const attached = stationSn !== sn;
28062
- if (attached && this.registry.serialForFrame(stationSn, cmd.channel) !== sn)
28063
- return Promise.reject(new Error("RTC command requires an unambiguous attached-device channel"));
28064
- const member = stationRaw.member?.admin_user_id;
28065
- const adminUserId = typeof member === "string" && member ? member : void 0;
28066
- const base = { stationSn, adminUserId, attached };
28067
- return this.rtc.dispatchCommand(native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base, cmd);
28068
- }
28069
- 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 });
28070
28398
  }
28071
28399
  /**
28072
28400
  * List + classify devices across all houses (mega API). Each device is tagged with its API backend
@@ -28300,13 +28628,38 @@ var EufyMega = class extends EventEmitter12 {
28300
28628
  this.schedulePoll();
28301
28629
  const failures = readiness.push.failed + readiness.mqtt.failed + readiness.wiredP2p.failed;
28302
28630
  this.settleRealtimeGeneration(generation, failures || startupFailed ? "partial" : "ready");
28631
+ if (readiness.push.failed)
28632
+ this.retryPush(generation);
28303
28633
  } catch (e) {
28304
28634
  this.reportError(e);
28305
28635
  readiness.push.failed += readiness.push.pending;
28306
28636
  readiness.push.pending = 0;
28307
28637
  this.settleRealtimeGeneration(generation, epoch === this.realtimeEpoch ? "partial" : "superseded");
28638
+ if (epoch === this.realtimeEpoch)
28639
+ this.retryPush(generation);
28308
28640
  }
28309
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
+ }
28310
28663
  /** Resolve a generation with an immutable count snapshot; later transport completions are ignored. */
28311
28664
  settleRealtimeGeneration(generation, state) {
28312
28665
  if (generation.settled)
@@ -29162,6 +29515,7 @@ var EufyMega = class extends EventEmitter12 {
29162
29515
  */
29163
29516
  async teardownRealtime() {
29164
29517
  this.pollTimer.cancel();
29518
+ this.pushRetryTimer.cancel();
29165
29519
  this.lastStateAnnounced.clear();
29166
29520
  await this.closeMqttTransports();
29167
29521
  this.rtc.close();
@@ -29669,7 +30023,7 @@ var SolixClient = class _SolixClient {
29669
30023
  };
29670
30024
 
29671
30025
  // dist/transport/mqtt/solix-mqtt.js
29672
- import { EventEmitter as EventEmitter13 } from "node:events";
30026
+ import { EventEmitter as EventEmitter14 } from "node:events";
29673
30027
  var SOLIX_METER_FIELD_NAMES = {
29674
30028
  168: "meterPowerL1",
29675
30029
  // Anker Modbus: primary_phase_1_active_power
@@ -29847,7 +30201,7 @@ function addSolarbankScalars(frame, out) {
29847
30201
  out.gridExportLimit = df.readUInt16LE(5);
29848
30202
  }
29849
30203
  }
29850
- var SolixMqtt = class extends EventEmitter13 {
30204
+ var SolixMqtt = class extends EventEmitter14 {
29851
30205
  transport;
29852
30206
  appName;
29853
30207
  userId;