@mega-yfue/eufy-sdk 0.5.0-beta.65 → 0.5.0-beta.67

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.
@@ -416,7 +416,7 @@ export declare class EufyMega extends EventEmitter {
416
416
  /**
417
417
  * The station a device's commands go through: the device itself, or the HomeBase the registry links it
418
418
  * to. Answers the target record, the station record and serial, the station's `device_type`, and the
419
- * `t9000` / `t8n00` family flags.
419
+ * `t9000` / `t8n00` / `t8223` family flags.
420
420
  */
421
421
  private stationOf;
422
422
  /** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
@@ -443,18 +443,21 @@ export declare class EufyMega extends EventEmitter {
443
443
  * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
444
444
  * station and the devices attached to it go over RTC, with an attached device's command refused
445
445
  * unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
446
- * relay candidates; T9000 retains compact SDP and relay-only candidates. Everything else is P2P.
446
+ * relay candidates; T9000 retains compact SDP and relay-only candidates. A T8223 doorbell's route serves
447
+ * media only, and the RTC router refuses a command on it. Everything else is P2P.
447
448
  */
448
449
  private routeCommand;
449
450
  /**
450
- * The RTC route for a T9000 or T8N00 station or a device attached to one, decided by the station's model
451
- * classification; `undefined` for any other device. A T8N00 route carries its native `call` signalling. The admin id is the station's
452
- * `member.admin_user_id`, else the logged-in user.
451
+ * The RTC route for a T9000 or T8N00 station or a device attached to one, or a T8223 doorbell that is its
452
+ * own station, decided by the station's model classification; `undefined` for any other device. A T8N00
453
+ * route carries its native `call` signalling; a T8223 route does too, and streams its video on a media
454
+ * track. The admin id is the station's `member.admin_user_id`, else the logged-in user.
453
455
  */
454
456
  private rtcRouteFor;
455
457
  /**
456
- * The RTC media provider for a camera attached to a T9000 whose `device_channel` resolves back to it on
457
- * the station; `undefined` otherwise, including a camera behind a T8N00, whose route carries commands only.
458
+ * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
459
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
460
+ * a T8N00, whose route carries commands only.
458
461
  */
459
462
  private rtcMediaFor;
460
463
  /**
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 EventEmitter13 } from "node:events";
8
+ import { EventEmitter as EventEmitter14 } from "node:events";
9
9
 
10
10
  // dist/transport/http/mega-client.js
11
11
  import { createHash as createHash3 } from "node:crypto";
@@ -3655,6 +3655,7 @@ var isIndoorPanTiltS350 = (ctx) => has(INDOOR_PT_S350_TYPES, ctx.deviceType);
3655
3655
  var isHomeBase = (ctx) => has(HOMEBASE_TYPES, ctx.deviceType);
3656
3656
  var isStation9000 = (ctx) => ctx.deviceType === DeviceType.STATION_9000;
3657
3657
  var isNvrT8N00 = (ctx) => ctx.model === "T8N00";
3658
+ var isDoorbellT8223 = (ctx) => ctx.model === "T8223";
3658
3659
  var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
3659
3660
  var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
3660
3661
 
@@ -7756,6 +7757,8 @@ var ARMING_CMD = {
7756
7757
  * The ENCRYPTION LEVEL is the session's to pick (`"auto"`), not this command's: the T8030 the envelope
7757
7758
  * was captured on holds a level-2 key and seals it level-2, while an own-session camera that never
7758
7759
  * negotiates one carries the same envelope level-1. See `armingCommand`.
7760
+ *
7761
+ * A T8410C takes the mode as a direct frame instead of this envelope; see {@link DIRECT_ARMING_MODELS}.
7759
7762
  */
7760
7763
  SET_ARMING: 1224,
7761
7764
  /**
@@ -7794,6 +7797,8 @@ var ARMING_CMD = {
7794
7797
  };
7795
7798
  var DEVICE_NOTIFY_FLAG = 8;
7796
7799
  var STANDALONE_ACTION_MODELS = /* @__PURE__ */ new Set(["T8170", "T8171"]);
7800
+ var DIRECT_ARMING_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
7801
+ var DIRECT_ARMING_MODES = /* @__PURE__ */ new Set(["away", "home", "disarmed"]);
7797
7802
  var STANDALONE_READ_MODELS = /* @__PURE__ */ new Set([...STANDALONE_ACTION_MODELS, "T8400", "T8442", "T8410"]);
7798
7803
  function ownAction(read, mode) {
7799
7804
  const value = Number(read(`${mode}Action`)?.value);
@@ -7826,6 +7831,12 @@ function armingCommand(mode, ctx) {
7826
7831
  if (!ctx.accountName) {
7827
7832
  throw new Error(`arming: missing account identity (user_name) [${describeDevice(ctx)}]`);
7828
7833
  }
7834
+ if (DIRECT_ARMING_MODELS.has(ctx.model ?? "")) {
7835
+ if (!DIRECT_ARMING_MODES.has(mode)) {
7836
+ throw new Error(`arming: mode ${mode} is unconfirmed on the direct 1224 frame [${describeDevice(ctx)}]`);
7837
+ }
7838
+ return setStationScalar(ARMING_CMD.SET_ARMING, ARMING_MODE_WIRE[mode], ctx.channel);
7839
+ }
7829
7840
  return setPayload(ARMING_CMD.SET_ARMING, { mode_type: ARMING_MODE_WIRE[mode], user_name: ctx.accountName }, ctx, 0, void 0, "auto");
7830
7841
  }
7831
7842
  function alarmDelayCommand(mode, config, ctx) {
@@ -7880,7 +7891,7 @@ var ARMING_MEMBERS = {
7880
7891
  kind: "enum",
7881
7892
  enumValues: ARMING_MODE_LABELS,
7882
7893
  provenance: "verified",
7883
- description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads and SETS all 9 modes the app defines \u2014 away/home/schedule/custom1/custom2/custom3/off/geo/disarmed. Three are byte-captured writes and six are live-confirmed (each sent and observed to report its own MODE_SWITCH); see ARMING_MODE_WIRE in arming.ts for the per-value evidence.",
7894
+ description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads and SETS all 9 modes the app defines \u2014 away/home/schedule/custom1/custom2/custom3/off/geo/disarmed. Three are byte-captured writes and six are live-confirmed (each sent and observed to report its own MODE_SWITCH); see ARMING_MODE_WIRE in arming.ts for the per-value evidence. On a T8410C only away, home and disarmed are writable.",
7884
7895
  observation: {
7885
7896
  event: "armingModeChanged",
7886
7897
  reflects: (value) => ({ param: ARMING_CMD.SET_ARMING, expected: ARMING_MODE_WIRE[armingModeOf(value)] }),
@@ -23356,11 +23367,139 @@ var RtcLiveStream = class extends EventEmitter7 {
23356
23367
  }
23357
23368
  };
23358
23369
 
23370
+ // dist/transport/rtc/call-live.js
23371
+ import { EventEmitter as EventEmitter8 } from "node:events";
23372
+ var START_CODE = Buffer.from([0, 0, 0, 1]);
23373
+ var H264Depacketizer = class {
23374
+ onUnit;
23375
+ nals = [];
23376
+ fu;
23377
+ lastSeq = -1;
23378
+ broken = false;
23379
+ constructor(onUnit) {
23380
+ this.onUnit = onUnit;
23381
+ }
23382
+ push(packet) {
23383
+ if (packet.length < 12 || packet[0] >> 6 !== 2)
23384
+ return;
23385
+ const pt = packet[1] & 127;
23386
+ if (pt >= 72 && pt <= 79)
23387
+ return;
23388
+ const marker = (packet[1] & 128) !== 0;
23389
+ const seq = packet.readUInt16BE(2);
23390
+ const timestamp = packet.readUInt32BE(4);
23391
+ if (this.lastSeq >= 0 && seq !== (this.lastSeq + 1 & 65535))
23392
+ this.broken = true;
23393
+ this.lastSeq = seq;
23394
+ const cc = packet[0] & 15;
23395
+ let off = 12 + cc * 4;
23396
+ if (packet[0] & 16) {
23397
+ if (packet.length < off + 4)
23398
+ return;
23399
+ off += 4 + packet.readUInt16BE(off + 2) * 4;
23400
+ }
23401
+ let end = packet.length;
23402
+ if (packet[0] & 32)
23403
+ end -= packet[packet.length - 1];
23404
+ if (end <= off)
23405
+ return;
23406
+ const p = packet.subarray(off, end);
23407
+ const type = p[0] & 31;
23408
+ if (type >= 1 && type <= 23) {
23409
+ this.nals.push(Buffer.from(p));
23410
+ } else if (type === 24) {
23411
+ let o = 1;
23412
+ while (o + 2 <= p.length) {
23413
+ const len = p.readUInt16BE(o);
23414
+ if (o + 2 + len > p.length)
23415
+ break;
23416
+ this.nals.push(Buffer.from(p.subarray(o + 2, o + 2 + len)));
23417
+ o += 2 + len;
23418
+ }
23419
+ } else if (type === 28 && p.length > 2) {
23420
+ const start = (p[1] & 128) !== 0;
23421
+ const stop = (p[1] & 64) !== 0;
23422
+ if (start)
23423
+ this.fu = [Buffer.from([p[0] & 224 | p[1] & 31])];
23424
+ if (this.fu)
23425
+ this.fu.push(Buffer.from(p.subarray(2)));
23426
+ else
23427
+ this.broken = true;
23428
+ if (stop && this.fu) {
23429
+ this.nals.push(Buffer.concat(this.fu));
23430
+ this.fu = void 0;
23431
+ }
23432
+ }
23433
+ if (marker) {
23434
+ if (!this.broken && !this.fu && this.nals.length) {
23435
+ const parts = [];
23436
+ for (const n of this.nals)
23437
+ parts.push(START_CODE, n);
23438
+ this.onUnit(Buffer.concat(parts), timestamp);
23439
+ }
23440
+ this.nals = [];
23441
+ this.fu = void 0;
23442
+ this.broken = false;
23443
+ }
23444
+ }
23445
+ };
23446
+ var RtcCallLiveStream = class extends EventEmitter8 {
23447
+ session;
23448
+ logger;
23449
+ running = false;
23450
+ depacketizer = new H264Depacketizer((unit, ts) => this.onUnit(unit, ts));
23451
+ /** `session` is a session on a {@link RtcPeerOptions.mediaTrack} peer, not yet connected. */
23452
+ constructor(session, logger) {
23453
+ super();
23454
+ this.session = session;
23455
+ this.logger = logger ?? noopLogger;
23456
+ }
23457
+ start() {
23458
+ if (this.running)
23459
+ return this;
23460
+ this.running = true;
23461
+ const { session } = this;
23462
+ session.on("rtp", (packet) => this.depacketizer.push(packet));
23463
+ session.on("close", () => this.fail(new Error("rtc call: session closed")));
23464
+ session.on("error", (e) => this.fail(e));
23465
+ session.connect().catch((e) => this.fail(e instanceof Error ? e : new Error(String(e))));
23466
+ return this;
23467
+ }
23468
+ stop() {
23469
+ if (!this.running)
23470
+ return;
23471
+ this.teardown();
23472
+ this.emit("stop");
23473
+ }
23474
+ onUnit(unit, timestamp) {
23475
+ const frame = {
23476
+ codec: "h264",
23477
+ data: unit,
23478
+ keyframe: hasIdr(unit, "h264"),
23479
+ width: 0,
23480
+ height: 0,
23481
+ sourceTimestamp: timestamp
23482
+ };
23483
+ this.emit("video", frame);
23484
+ }
23485
+ fail(err) {
23486
+ if (!this.running)
23487
+ return;
23488
+ this.logger.warn(`[rtc-call] ${err.message}`);
23489
+ this.teardown();
23490
+ this.emit("error", err);
23491
+ }
23492
+ teardown() {
23493
+ this.running = false;
23494
+ this.session.close();
23495
+ }
23496
+ };
23497
+
23359
23498
  // dist/transport/rtc/session.js
23360
- import { EventEmitter as EventEmitter10 } from "node:events";
23499
+ import { EventEmitter as EventEmitter11 } from "node:events";
23361
23500
 
23362
23501
  // dist/transport/rtc/peer.js
23363
- import { EventEmitter as EventEmitter8 } from "node:events";
23502
+ import { EventEmitter as EventEmitter9 } from "node:events";
23364
23503
 
23365
23504
  // dist/transport/rtc/ptcs-framer.js
23366
23505
  var MAGIC3 = Buffer.from("PTCS", "ascii");
@@ -23611,6 +23750,17 @@ function forceDtlsRole(sdp, role) {
23611
23750
  function toWireCandidate(candidate) {
23612
23751
  return candidate.trim().replace(/^a=/, "");
23613
23752
  }
23753
+ function midOf(sdp, kind) {
23754
+ const section = sdp.split(/(?=^m=)/m).find((s) => s.startsWith(`m=${kind} `));
23755
+ return section?.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim();
23756
+ }
23757
+ function rejectDataSection(sdp) {
23758
+ const appMid = midOf(sdp, "application");
23759
+ const out = sdp.replace(/^m=application \d+/m, "m=application 0");
23760
+ if (appMid === void 0)
23761
+ return out;
23762
+ return out.replace(/^(a=group:BUNDLE)([^\r\n]*)/m, (_, head, mids) => [head, ...mids.split(" ").filter((t) => t && t !== appMid)].join(" "));
23763
+ }
23614
23764
 
23615
23765
  // dist/transport/rtc/peer.js
23616
23766
  var DATA_CHANNEL_LABELS = ["WebrtcDataChannel", "audio", "idr", "video", "notify", "download"];
@@ -23645,7 +23795,7 @@ async function loadNativePeerFactory() {
23645
23795
  return (name, config) => new ndc.PeerConnection(name, config);
23646
23796
  }
23647
23797
  var ANSWER_TIMEOUT_MS = 15e3;
23648
- var RtcPeer = class extends EventEmitter8 {
23798
+ var RtcPeer = class extends EventEmitter9 {
23649
23799
  opts;
23650
23800
  pc;
23651
23801
  channels = /* @__PURE__ */ new Map();
@@ -23656,6 +23806,8 @@ var RtcPeer = class extends EventEmitter8 {
23656
23806
  remoteMid = HUB_SDP_MID;
23657
23807
  handlingOffer = false;
23658
23808
  channelsCreated = false;
23809
+ videoMid;
23810
+ videoTrack;
23659
23811
  gatheringDone = false;
23660
23812
  /** Set by the framer's wire callback when the native channel refused a packet mid-send. */
23661
23813
  wireSendFailed = false;
@@ -23705,6 +23857,8 @@ var RtcPeer = class extends EventEmitter8 {
23705
23857
  this.emit("connectionState", state);
23706
23858
  });
23707
23859
  pc.onDataChannel((dc) => this.wireChannel(dc.getLabel(), dc));
23860
+ if (this.opts.mediaTrack)
23861
+ pc.onTrack((track) => this.wireVideoTrack(track));
23708
23862
  }
23709
23863
  /**
23710
23864
  * The hub offered: apply the offer, declare the channels, return the answer SDP to signal back.
@@ -23714,7 +23868,9 @@ var RtcPeer = class extends EventEmitter8 {
23714
23868
  * only what is announced. The offer goes in before the channels are declared: `createDataChannel` on
23715
23869
  * a peer without a remote description starts its own negotiation and leaves it in `have-local-offer`.
23716
23870
  * The data channels need no m-line of their own; the offer's SCTP m-line carries them. Only an ANSWER
23717
- * is taken as the local description: one read in `have-local-offer` is an offer.
23871
+ * is taken as the local description: one read in `have-local-offer` is an offer. A
23872
+ * {@link RtcPeerOptions.mediaTrack} peer applies the offer with its application section rejected,
23873
+ * declares no channels, and adds remote candidates on the offer's first (bundle-tagged) mid.
23718
23874
  */
23719
23875
  async handleRemoteOffer(offerSdp) {
23720
23876
  const pc = this.pc;
@@ -23724,9 +23880,12 @@ var RtcPeer = class extends EventEmitter8 {
23724
23880
  throw new Error("RTC peer already handling an offer");
23725
23881
  this.handlingOffer = true;
23726
23882
  try {
23727
- const offer = forceDtlsRole(offerSdp, "passive");
23728
- const application = offer.split(/(?=^m=)/m).find((section) => section.startsWith("m=application "));
23729
- this.remoteMid = application?.match(/^a=mid:([^\r\n]+)/m)?.[1] ?? HUB_SDP_MID;
23883
+ const media = this.opts.mediaTrack === true;
23884
+ const pinned = forceDtlsRole(offerSdp, "passive");
23885
+ const offer = media ? rejectDataSection(pinned) : pinned;
23886
+ this.remoteMid = media ? offer.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim() ?? HUB_SDP_MID : midOf(offer, "application") ?? HUB_SDP_MID;
23887
+ if (media)
23888
+ this.videoMid = midOf(offer, "video");
23730
23889
  const answerWait = new Promise((resolve, reject) => {
23731
23890
  const timer = setTimeout(() => {
23732
23891
  this.localAnswer = void 0;
@@ -23736,7 +23895,8 @@ var RtcPeer = class extends EventEmitter8 {
23736
23895
  });
23737
23896
  pc.setRemoteDescription(offer, "offer");
23738
23897
  this.remoteSet = true;
23739
- this.createChannels();
23898
+ if (!media)
23899
+ this.createChannels();
23740
23900
  const local = pc.localDescription();
23741
23901
  let answer = String(local?.type ?? "").toLowerCase() === "answer" ? local?.sdp ?? "" : "";
23742
23902
  if (!answer)
@@ -23805,6 +23965,7 @@ var RtcPeer = class extends EventEmitter8 {
23805
23965
  this.pc = void 0;
23806
23966
  this.channels.clear();
23807
23967
  this.channelsCreated = false;
23968
+ this.videoTrack = void 0;
23808
23969
  this.remoteSet = false;
23809
23970
  this.pending.length = 0;
23810
23971
  }
@@ -23868,6 +24029,14 @@ var RtcPeer = class extends EventEmitter8 {
23868
24029
  this.framer?.recvPacket(buf);
23869
24030
  });
23870
24031
  }
24032
+ /** Take the offer's video m-line as the one track whose RTP is emitted; any other track is left alone. */
24033
+ wireVideoTrack(track) {
24034
+ if (this.videoTrack || this.videoMid === void 0 || track.mid() !== this.videoMid)
24035
+ return;
24036
+ this.videoTrack = track;
24037
+ this.logger.debug(`[rtc] video track ${this.videoMid}`);
24038
+ track.onMessage((msg) => this.emit("rtp", Buffer.isBuffer(msg) ? msg : Buffer.from(msg)));
24039
+ }
23871
24040
  addNow(candidate) {
23872
24041
  try {
23873
24042
  this.pc?.addRemoteCandidate(candidate, this.remoteMid);
@@ -23883,7 +24052,7 @@ var RtcPeer = class extends EventEmitter8 {
23883
24052
  };
23884
24053
 
23885
24054
  // dist/transport/rtc/signaling.js
23886
- import { EventEmitter as EventEmitter9 } from "node:events";
24055
+ import { EventEmitter as EventEmitter10 } from "node:events";
23887
24056
  import { createHmac as createHmac3, randomUUID as randomUUID2 } from "node:crypto";
23888
24057
  var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
23889
24058
  var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
@@ -23899,7 +24068,7 @@ var defaultSocket = (url, init) => new WebSocket(url, {
23899
24068
  protocols: init.protocols,
23900
24069
  headers: { Origin: init.origin }
23901
24070
  });
23902
- var RtcSignalingClient = class extends EventEmitter9 {
24071
+ var RtcSignalingClient = class extends EventEmitter10 {
23903
24072
  opts;
23904
24073
  ws;
23905
24074
  sign;
@@ -24124,7 +24293,7 @@ var RtcSignalingClient = class extends EventEmitter9 {
24124
24293
 
24125
24294
  // dist/transport/rtc/session.js
24126
24295
  var AUTH_TIMEOUT_MS = 15e3;
24127
- var RtcSession = class extends EventEmitter10 {
24296
+ var RtcSession = class extends EventEmitter11 {
24128
24297
  opts;
24129
24298
  signaling;
24130
24299
  peer;
@@ -24143,6 +24312,7 @@ var RtcSession = class extends EventEmitter10 {
24143
24312
  this.logger = opts.logger ?? noopLogger;
24144
24313
  this.signaling = (opts.createSignaling ?? ((o) => new RtcSignalingClient(o)))(opts);
24145
24314
  this.peer = (opts.createPeer ?? ((o) => new RtcPeer(o)))({ logger: this.logger, ...opts.peer });
24315
+ const mediaTrack = opts.peer?.mediaTrack === true;
24146
24316
  this.signaling.on("message", (inner) => {
24147
24317
  this.chain = this.chain.then(() => this.onSignaling(inner)).catch((e) => {
24148
24318
  this.emit("error", e instanceof Error ? e : new Error(String(e)));
@@ -24170,13 +24340,14 @@ var RtcSession = class extends EventEmitter10 {
24170
24340
  this.announceClose();
24171
24341
  });
24172
24342
  this.peer.on("connectionState", (state) => {
24173
- if ((state === "failed" || state === "closed") && this.connected) {
24343
+ if ((state === "failed" || state === "closed") && (this.connected || mediaTrack)) {
24174
24344
  this.connected = false;
24175
24345
  this.announceClose();
24176
24346
  }
24177
24347
  });
24178
24348
  this.peer.on("error", (e) => this.emit("error", e));
24179
24349
  this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
24350
+ this.peer.on("rtp", (packet) => this.emit("rtp", packet));
24180
24351
  this.peer.on("data", (frame, linkType) => {
24181
24352
  if (linkType === PortalLinkType.LIVE)
24182
24353
  this.emit("mediaData", frame);
@@ -24373,6 +24544,7 @@ var RtcCommandRouter = class {
24373
24544
  sessions = /* @__PURE__ */ new Map();
24374
24545
  lives = /* @__PURE__ */ new Map();
24375
24546
  warm = /* @__PURE__ */ new Map();
24547
+ callSources = /* @__PURE__ */ new Map();
24376
24548
  constructor(deps) {
24377
24549
  this.deps = deps;
24378
24550
  }
@@ -24380,6 +24552,8 @@ var RtcCommandRouter = class {
24380
24552
  if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
24381
24553
  throw new Error(`rtc: ${cmd.kind} is not routable over the RTC control channel (only set-payload or set-json)`);
24382
24554
  }
24555
+ if (route.mediaTrack)
24556
+ throw new Error(`rtc: ${route.stationSn} serves media only and takes no portal command`);
24383
24557
  const identity = this.deps.identity();
24384
24558
  if (!identity)
24385
24559
  throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
@@ -24414,11 +24588,56 @@ var RtcCommandRouter = class {
24414
24588
  }
24415
24589
  };
24416
24590
  }
24591
+ /**
24592
+ * Live view of a station on a {@link RtcRoute.mediaTrack} route: a live feed, a readable feed and a live
24593
+ * still, from one shared source per station whose every warm-up places a fresh `call` on its own
24594
+ * session. Recording has no wire here and is refused.
24595
+ */
24596
+ trackMediaProviderFor(route) {
24597
+ const sn = route.stationSn;
24598
+ const sourceFor = (hints = {}) => {
24599
+ let source = this.callSources.get(sn);
24600
+ if (!source) {
24601
+ source = new SharedLiveSource({
24602
+ makeStream: () => {
24603
+ const identity = this.deps.identity();
24604
+ if (!identity)
24605
+ throw new Error(`rtc: not logged in, cannot call ${sn}`);
24606
+ const session = this.newSession(route, route.adminUserId || identity.userId, identity);
24607
+ return new RtcCallLiveStream(session, this.deps.logger);
24608
+ },
24609
+ label: sn,
24610
+ logger: this.deps.logger,
24611
+ powered: hints.powered,
24612
+ preBufferSeconds: hints.preBufferSeconds,
24613
+ warmTimeoutMs: 45e3
24614
+ });
24615
+ this.callSources.set(sn, source);
24616
+ }
24617
+ return source;
24618
+ };
24619
+ return {
24620
+ live: async (opts) => attachUnlessAborted(sourceFor(opts), opts?.signal),
24621
+ openReadable: async (opts) => openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts),
24622
+ snapshotLive: (opts) => captureSnapshotFromShared(sourceFor(opts), {
24623
+ ...opts,
24624
+ logger: this.deps.logger,
24625
+ ffmpegPath: this.deps.ffmpegPath
24626
+ }),
24627
+ record: async () => {
24628
+ throw new Error(`record is not available for ${sn} over a portal call`);
24629
+ }
24630
+ };
24631
+ }
24417
24632
  /**
24418
24633
  * Tear down every station session (logout / shutdown). Each live source is disposed before its session
24419
24634
  * closes, so its consumers end cleanly and its stop still rides the session.
24420
24635
  */
24421
24636
  close() {
24637
+ for (const [sn, src] of this.callSources) {
24638
+ this.callSources.delete(sn);
24639
+ src.dispose();
24640
+ }
24422
24641
  for (const [sn, live] of this.lives) {
24423
24642
  this.lives.delete(sn);
24424
24643
  live.source.dispose();
@@ -24548,6 +24767,19 @@ var RtcCommandRouter = class {
24548
24767
  }
24549
24768
  });
24550
24769
  }
24770
+ newSession(route, adminUserId, identity) {
24771
+ return (this.deps.createSession ?? ((o) => new RtcSession(o)))({
24772
+ authToken: identity.authToken,
24773
+ gtoken: identity.gtoken,
24774
+ stationSn: route.stationSn,
24775
+ adminUserId,
24776
+ shard: this.deps.shard(),
24777
+ country: this.deps.country ?? "US",
24778
+ logger: this.deps.logger,
24779
+ signalingMode: route.signalingMode,
24780
+ peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy, mediaTrack: route.mediaTrack }
24781
+ });
24782
+ }
24551
24783
  async stationSession(route, adminUserId, identity) {
24552
24784
  const sn = route.stationSn;
24553
24785
  const existing = this.sessions.get(sn);
@@ -24559,17 +24791,7 @@ var RtcCommandRouter = class {
24559
24791
  }
24560
24792
  this.drop(sn, existing);
24561
24793
  }
24562
- const session = (this.deps.createSession ?? ((o) => new RtcSession(o)))({
24563
- authToken: identity.authToken,
24564
- gtoken: identity.gtoken,
24565
- stationSn: sn,
24566
- adminUserId,
24567
- shard: this.deps.shard(),
24568
- country: this.deps.country ?? "US",
24569
- logger: this.deps.logger,
24570
- signalingMode: route.signalingMode,
24571
- peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
24572
- });
24794
+ const session = this.newSession(route, adminUserId, identity);
24573
24795
  const ready = this.bringUp(sn, session);
24574
24796
  const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
24575
24797
  session.on("error", (e) => this.deps.onError?.(e));
@@ -26380,7 +26602,7 @@ async function resolveLightEffect(mega, lightId) {
26380
26602
  }
26381
26603
 
26382
26604
  // dist/transport/push/push-client.js
26383
- import { EventEmitter as EventEmitter12 } from "node:events";
26605
+ import { EventEmitter as EventEmitter13 } from "node:events";
26384
26606
  import tls2 from "node:tls";
26385
26607
 
26386
26608
  // dist/transport/push/proto.js
@@ -26635,7 +26857,7 @@ var MessageTag;
26635
26857
  })(MessageTag || (MessageTag = {}));
26636
26858
 
26637
26859
  // dist/transport/push/parser.js
26638
- import { EventEmitter as EventEmitter11 } from "node:events";
26860
+ import { EventEmitter as EventEmitter12 } from "node:events";
26639
26861
  var State;
26640
26862
  (function(State2) {
26641
26863
  State2[State2["VERSION_TAG_AND_SIZE"] = 0] = "VERSION_TAG_AND_SIZE";
@@ -26653,7 +26875,7 @@ var TAG_TO_TYPE = {
26653
26875
  [MessageTag.DataMessageStanza]: "mcs_proto.DataMessageStanza",
26654
26876
  [MessageTag.StreamErrorStanza]: "mcs_proto.StreamErrorStanza"
26655
26877
  };
26656
- var McsParser = class extends EventEmitter11 {
26878
+ var McsParser = class extends EventEmitter12 {
26657
26879
  data = Buffer.alloc(0);
26658
26880
  state = State.VERSION_TAG_AND_SIZE;
26659
26881
  messageTag = 0;
@@ -26831,7 +27053,7 @@ function normalizePushEvent(raw) {
26831
27053
  raw
26832
27054
  };
26833
27055
  }
26834
- var PushClient = class _PushClient extends EventEmitter12 {
27056
+ var PushClient = class _PushClient extends EventEmitter13 {
26835
27057
  creds;
26836
27058
  logger;
26837
27059
  /** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
@@ -28038,7 +28260,7 @@ function readinessSnapshot(readiness, state = readiness.state) {
28038
28260
  wiredP2p: Object.freeze({ ...readiness.wiredP2p })
28039
28261
  });
28040
28262
  }
28041
- var EufyMega = class extends EventEmitter13 {
28263
+ var EufyMega = class extends EventEmitter14 {
28042
28264
  opts;
28043
28265
  mega;
28044
28266
  /**
@@ -28905,7 +29127,7 @@ var EufyMega = class extends EventEmitter13 {
28905
29127
  /**
28906
29128
  * The station a device's commands go through: the device itself, or the HomeBase the registry links it
28907
29129
  * to. Answers the target record, the station record and serial, the station's `device_type`, and the
28908
- * `t9000` / `t8n00` family flags.
29130
+ * `t9000` / `t8n00` / `t8223` family flags.
28909
29131
  */
28910
29132
  stationOf(sn) {
28911
29133
  const devices = this.registry.list();
@@ -28915,7 +29137,16 @@ var EufyMega = class extends EventEmitter13 {
28915
29137
  const raw = station?.raw ?? {};
28916
29138
  const deviceType = typeof raw.device_type === "number" ? raw.device_type : void 0;
28917
29139
  const family = { deviceType, model: station?.model };
28918
- return { target, station, stationSn, raw, deviceType, t9000: isStation9000(family), t8n00: isNvrT8N00(family) };
29140
+ return {
29141
+ target,
29142
+ station,
29143
+ stationSn,
29144
+ raw,
29145
+ deviceType,
29146
+ t9000: isStation9000(family),
29147
+ t8n00: isNvrT8N00(family),
29148
+ t8223: isDoorbellT8223(family)
29149
+ };
28919
29150
  }
28920
29151
  /** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
28921
29152
  stationDeviceTypeOf(sn) {
@@ -28974,7 +29205,8 @@ var EufyMega = class extends EventEmitter13 {
28974
29205
  * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
28975
29206
  * station and the devices attached to it go over RTC, with an attached device's command refused
28976
29207
  * unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
28977
- * relay candidates; T9000 retains compact SDP and relay-only candidates. Everything else is P2P.
29208
+ * relay candidates; T9000 retains compact SDP and relay-only candidates. A T8223 doorbell's route serves
29209
+ * media only, and the RTC router refuses a command on it. Everything else is P2P.
28978
29210
  */
28979
29211
  routeCommand(sn, cmd) {
28980
29212
  if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
@@ -29003,25 +29235,32 @@ var EufyMega = class extends EventEmitter13 {
29003
29235
  return this.p2p.dispatchCommand(sn, cmd);
29004
29236
  }
29005
29237
  /**
29006
- * The RTC route for a T9000 or T8N00 station or a device attached to one, decided by the station's model
29007
- * classification; `undefined` for any other device. A T8N00 route carries its native `call` signalling. The admin id is the station's
29008
- * `member.admin_user_id`, else the logged-in user.
29238
+ * The RTC route for a T9000 or T8N00 station or a device attached to one, or a T8223 doorbell that is its
29239
+ * own station, decided by the station's model classification; `undefined` for any other device. A T8N00
29240
+ * route carries its native `call` signalling; a T8223 route does too, and streams its video on a media
29241
+ * track. The admin id is the station's `member.admin_user_id`, else the logged-in user.
29009
29242
  */
29010
29243
  rtcRouteFor(sn) {
29011
- const { target, station, stationSn, raw, t9000, t8n00: native } = this.stationOf(sn);
29012
- if (!target || !station || !(t9000 || native))
29244
+ const { target, station, stationSn, raw, t9000, t8n00: native, t8223 } = this.stationOf(sn);
29245
+ const doorbell = t8223 && stationSn === sn;
29246
+ if (!target || !station || !(t9000 || native || doorbell))
29013
29247
  return void 0;
29014
29248
  const member = raw.member?.admin_user_id;
29015
29249
  const adminUserId = typeof member === "string" && member ? member : void 0;
29016
29250
  const base = { stationSn, adminUserId, attached: stationSn !== sn };
29251
+ if (doorbell)
29252
+ return { ...base, signalingMode: "call", iceTransportPolicy: "all", mediaTrack: true };
29017
29253
  return native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base;
29018
29254
  }
29019
29255
  /**
29020
- * The RTC media provider for a camera attached to a T9000 whose `device_channel` resolves back to it on
29021
- * the station; `undefined` otherwise, including a camera behind a T8N00, whose route carries commands only.
29256
+ * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
29257
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
29258
+ * a T8N00, whose route carries commands only.
29022
29259
  */
29023
29260
  rtcMediaFor(sn) {
29024
29261
  const route = this.rtcRouteFor(sn);
29262
+ if (route?.mediaTrack)
29263
+ return this.rtc.trackMediaProviderFor(route);
29025
29264
  if (!route?.attached || route.signalingMode === "call")
29026
29265
  return void 0;
29027
29266
  const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
@@ -30662,7 +30901,7 @@ var SolixClient = class _SolixClient {
30662
30901
  };
30663
30902
 
30664
30903
  // dist/transport/mqtt/solix-mqtt.js
30665
- import { EventEmitter as EventEmitter14 } from "node:events";
30904
+ import { EventEmitter as EventEmitter15 } from "node:events";
30666
30905
  var SOLIX_METER_FIELD_NAMES = {
30667
30906
  168: "meterPowerL1",
30668
30907
  // Anker Modbus: primary_phase_1_active_power
@@ -30840,7 +31079,7 @@ function addSolarbankScalars(frame, out) {
30840
31079
  out.gridExportLimit = df.readUInt16LE(5);
30841
31080
  }
30842
31081
  }
30843
- var SolixMqtt = class extends EventEmitter14 {
31082
+ var SolixMqtt = class extends EventEmitter15 {
30844
31083
  transport;
30845
31084
  appName;
30846
31085
  userId;