@mega-yfue/eufy-sdk 0.5.0-beta.66 → 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
 
@@ -23366,11 +23367,139 @@ var RtcLiveStream = class extends EventEmitter7 {
23366
23367
  }
23367
23368
  };
23368
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
+
23369
23498
  // dist/transport/rtc/session.js
23370
- import { EventEmitter as EventEmitter10 } from "node:events";
23499
+ import { EventEmitter as EventEmitter11 } from "node:events";
23371
23500
 
23372
23501
  // dist/transport/rtc/peer.js
23373
- import { EventEmitter as EventEmitter8 } from "node:events";
23502
+ import { EventEmitter as EventEmitter9 } from "node:events";
23374
23503
 
23375
23504
  // dist/transport/rtc/ptcs-framer.js
23376
23505
  var MAGIC3 = Buffer.from("PTCS", "ascii");
@@ -23621,6 +23750,17 @@ function forceDtlsRole(sdp, role) {
23621
23750
  function toWireCandidate(candidate) {
23622
23751
  return candidate.trim().replace(/^a=/, "");
23623
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
+ }
23624
23764
 
23625
23765
  // dist/transport/rtc/peer.js
23626
23766
  var DATA_CHANNEL_LABELS = ["WebrtcDataChannel", "audio", "idr", "video", "notify", "download"];
@@ -23655,7 +23795,7 @@ async function loadNativePeerFactory() {
23655
23795
  return (name, config) => new ndc.PeerConnection(name, config);
23656
23796
  }
23657
23797
  var ANSWER_TIMEOUT_MS = 15e3;
23658
- var RtcPeer = class extends EventEmitter8 {
23798
+ var RtcPeer = class extends EventEmitter9 {
23659
23799
  opts;
23660
23800
  pc;
23661
23801
  channels = /* @__PURE__ */ new Map();
@@ -23666,6 +23806,8 @@ var RtcPeer = class extends EventEmitter8 {
23666
23806
  remoteMid = HUB_SDP_MID;
23667
23807
  handlingOffer = false;
23668
23808
  channelsCreated = false;
23809
+ videoMid;
23810
+ videoTrack;
23669
23811
  gatheringDone = false;
23670
23812
  /** Set by the framer's wire callback when the native channel refused a packet mid-send. */
23671
23813
  wireSendFailed = false;
@@ -23715,6 +23857,8 @@ var RtcPeer = class extends EventEmitter8 {
23715
23857
  this.emit("connectionState", state);
23716
23858
  });
23717
23859
  pc.onDataChannel((dc) => this.wireChannel(dc.getLabel(), dc));
23860
+ if (this.opts.mediaTrack)
23861
+ pc.onTrack((track) => this.wireVideoTrack(track));
23718
23862
  }
23719
23863
  /**
23720
23864
  * The hub offered: apply the offer, declare the channels, return the answer SDP to signal back.
@@ -23724,7 +23868,9 @@ var RtcPeer = class extends EventEmitter8 {
23724
23868
  * only what is announced. The offer goes in before the channels are declared: `createDataChannel` on
23725
23869
  * a peer without a remote description starts its own negotiation and leaves it in `have-local-offer`.
23726
23870
  * The data channels need no m-line of their own; the offer's SCTP m-line carries them. Only an ANSWER
23727
- * 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.
23728
23874
  */
23729
23875
  async handleRemoteOffer(offerSdp) {
23730
23876
  const pc = this.pc;
@@ -23734,9 +23880,12 @@ var RtcPeer = class extends EventEmitter8 {
23734
23880
  throw new Error("RTC peer already handling an offer");
23735
23881
  this.handlingOffer = true;
23736
23882
  try {
23737
- const offer = forceDtlsRole(offerSdp, "passive");
23738
- const application = offer.split(/(?=^m=)/m).find((section) => section.startsWith("m=application "));
23739
- 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");
23740
23889
  const answerWait = new Promise((resolve, reject) => {
23741
23890
  const timer = setTimeout(() => {
23742
23891
  this.localAnswer = void 0;
@@ -23746,7 +23895,8 @@ var RtcPeer = class extends EventEmitter8 {
23746
23895
  });
23747
23896
  pc.setRemoteDescription(offer, "offer");
23748
23897
  this.remoteSet = true;
23749
- this.createChannels();
23898
+ if (!media)
23899
+ this.createChannels();
23750
23900
  const local = pc.localDescription();
23751
23901
  let answer = String(local?.type ?? "").toLowerCase() === "answer" ? local?.sdp ?? "" : "";
23752
23902
  if (!answer)
@@ -23815,6 +23965,7 @@ var RtcPeer = class extends EventEmitter8 {
23815
23965
  this.pc = void 0;
23816
23966
  this.channels.clear();
23817
23967
  this.channelsCreated = false;
23968
+ this.videoTrack = void 0;
23818
23969
  this.remoteSet = false;
23819
23970
  this.pending.length = 0;
23820
23971
  }
@@ -23878,6 +24029,14 @@ var RtcPeer = class extends EventEmitter8 {
23878
24029
  this.framer?.recvPacket(buf);
23879
24030
  });
23880
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
+ }
23881
24040
  addNow(candidate) {
23882
24041
  try {
23883
24042
  this.pc?.addRemoteCandidate(candidate, this.remoteMid);
@@ -23893,7 +24052,7 @@ var RtcPeer = class extends EventEmitter8 {
23893
24052
  };
23894
24053
 
23895
24054
  // dist/transport/rtc/signaling.js
23896
- import { EventEmitter as EventEmitter9 } from "node:events";
24055
+ import { EventEmitter as EventEmitter10 } from "node:events";
23897
24056
  import { createHmac as createHmac3, randomUUID as randomUUID2 } from "node:crypto";
23898
24057
  var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
23899
24058
  var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
@@ -23909,7 +24068,7 @@ var defaultSocket = (url, init) => new WebSocket(url, {
23909
24068
  protocols: init.protocols,
23910
24069
  headers: { Origin: init.origin }
23911
24070
  });
23912
- var RtcSignalingClient = class extends EventEmitter9 {
24071
+ var RtcSignalingClient = class extends EventEmitter10 {
23913
24072
  opts;
23914
24073
  ws;
23915
24074
  sign;
@@ -24134,7 +24293,7 @@ var RtcSignalingClient = class extends EventEmitter9 {
24134
24293
 
24135
24294
  // dist/transport/rtc/session.js
24136
24295
  var AUTH_TIMEOUT_MS = 15e3;
24137
- var RtcSession = class extends EventEmitter10 {
24296
+ var RtcSession = class extends EventEmitter11 {
24138
24297
  opts;
24139
24298
  signaling;
24140
24299
  peer;
@@ -24153,6 +24312,7 @@ var RtcSession = class extends EventEmitter10 {
24153
24312
  this.logger = opts.logger ?? noopLogger;
24154
24313
  this.signaling = (opts.createSignaling ?? ((o) => new RtcSignalingClient(o)))(opts);
24155
24314
  this.peer = (opts.createPeer ?? ((o) => new RtcPeer(o)))({ logger: this.logger, ...opts.peer });
24315
+ const mediaTrack = opts.peer?.mediaTrack === true;
24156
24316
  this.signaling.on("message", (inner) => {
24157
24317
  this.chain = this.chain.then(() => this.onSignaling(inner)).catch((e) => {
24158
24318
  this.emit("error", e instanceof Error ? e : new Error(String(e)));
@@ -24180,13 +24340,14 @@ var RtcSession = class extends EventEmitter10 {
24180
24340
  this.announceClose();
24181
24341
  });
24182
24342
  this.peer.on("connectionState", (state) => {
24183
- if ((state === "failed" || state === "closed") && this.connected) {
24343
+ if ((state === "failed" || state === "closed") && (this.connected || mediaTrack)) {
24184
24344
  this.connected = false;
24185
24345
  this.announceClose();
24186
24346
  }
24187
24347
  });
24188
24348
  this.peer.on("error", (e) => this.emit("error", e));
24189
24349
  this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
24350
+ this.peer.on("rtp", (packet) => this.emit("rtp", packet));
24190
24351
  this.peer.on("data", (frame, linkType) => {
24191
24352
  if (linkType === PortalLinkType.LIVE)
24192
24353
  this.emit("mediaData", frame);
@@ -24383,6 +24544,7 @@ var RtcCommandRouter = class {
24383
24544
  sessions = /* @__PURE__ */ new Map();
24384
24545
  lives = /* @__PURE__ */ new Map();
24385
24546
  warm = /* @__PURE__ */ new Map();
24547
+ callSources = /* @__PURE__ */ new Map();
24386
24548
  constructor(deps) {
24387
24549
  this.deps = deps;
24388
24550
  }
@@ -24390,6 +24552,8 @@ var RtcCommandRouter = class {
24390
24552
  if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
24391
24553
  throw new Error(`rtc: ${cmd.kind} is not routable over the RTC control channel (only set-payload or set-json)`);
24392
24554
  }
24555
+ if (route.mediaTrack)
24556
+ throw new Error(`rtc: ${route.stationSn} serves media only and takes no portal command`);
24393
24557
  const identity = this.deps.identity();
24394
24558
  if (!identity)
24395
24559
  throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
@@ -24424,11 +24588,56 @@ var RtcCommandRouter = class {
24424
24588
  }
24425
24589
  };
24426
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
+ }
24427
24632
  /**
24428
24633
  * Tear down every station session (logout / shutdown). Each live source is disposed before its session
24429
24634
  * closes, so its consumers end cleanly and its stop still rides the session.
24430
24635
  */
24431
24636
  close() {
24637
+ for (const [sn, src] of this.callSources) {
24638
+ this.callSources.delete(sn);
24639
+ src.dispose();
24640
+ }
24432
24641
  for (const [sn, live] of this.lives) {
24433
24642
  this.lives.delete(sn);
24434
24643
  live.source.dispose();
@@ -24558,6 +24767,19 @@ var RtcCommandRouter = class {
24558
24767
  }
24559
24768
  });
24560
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
+ }
24561
24783
  async stationSession(route, adminUserId, identity) {
24562
24784
  const sn = route.stationSn;
24563
24785
  const existing = this.sessions.get(sn);
@@ -24569,17 +24791,7 @@ var RtcCommandRouter = class {
24569
24791
  }
24570
24792
  this.drop(sn, existing);
24571
24793
  }
24572
- const session = (this.deps.createSession ?? ((o) => new RtcSession(o)))({
24573
- authToken: identity.authToken,
24574
- gtoken: identity.gtoken,
24575
- stationSn: sn,
24576
- adminUserId,
24577
- shard: this.deps.shard(),
24578
- country: this.deps.country ?? "US",
24579
- logger: this.deps.logger,
24580
- signalingMode: route.signalingMode,
24581
- peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
24582
- });
24794
+ const session = this.newSession(route, adminUserId, identity);
24583
24795
  const ready = this.bringUp(sn, session);
24584
24796
  const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
24585
24797
  session.on("error", (e) => this.deps.onError?.(e));
@@ -26390,7 +26602,7 @@ async function resolveLightEffect(mega, lightId) {
26390
26602
  }
26391
26603
 
26392
26604
  // dist/transport/push/push-client.js
26393
- import { EventEmitter as EventEmitter12 } from "node:events";
26605
+ import { EventEmitter as EventEmitter13 } from "node:events";
26394
26606
  import tls2 from "node:tls";
26395
26607
 
26396
26608
  // dist/transport/push/proto.js
@@ -26645,7 +26857,7 @@ var MessageTag;
26645
26857
  })(MessageTag || (MessageTag = {}));
26646
26858
 
26647
26859
  // dist/transport/push/parser.js
26648
- import { EventEmitter as EventEmitter11 } from "node:events";
26860
+ import { EventEmitter as EventEmitter12 } from "node:events";
26649
26861
  var State;
26650
26862
  (function(State2) {
26651
26863
  State2[State2["VERSION_TAG_AND_SIZE"] = 0] = "VERSION_TAG_AND_SIZE";
@@ -26663,7 +26875,7 @@ var TAG_TO_TYPE = {
26663
26875
  [MessageTag.DataMessageStanza]: "mcs_proto.DataMessageStanza",
26664
26876
  [MessageTag.StreamErrorStanza]: "mcs_proto.StreamErrorStanza"
26665
26877
  };
26666
- var McsParser = class extends EventEmitter11 {
26878
+ var McsParser = class extends EventEmitter12 {
26667
26879
  data = Buffer.alloc(0);
26668
26880
  state = State.VERSION_TAG_AND_SIZE;
26669
26881
  messageTag = 0;
@@ -26841,7 +27053,7 @@ function normalizePushEvent(raw) {
26841
27053
  raw
26842
27054
  };
26843
27055
  }
26844
- var PushClient = class _PushClient extends EventEmitter12 {
27056
+ var PushClient = class _PushClient extends EventEmitter13 {
26845
27057
  creds;
26846
27058
  logger;
26847
27059
  /** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
@@ -28048,7 +28260,7 @@ function readinessSnapshot(readiness, state = readiness.state) {
28048
28260
  wiredP2p: Object.freeze({ ...readiness.wiredP2p })
28049
28261
  });
28050
28262
  }
28051
- var EufyMega = class extends EventEmitter13 {
28263
+ var EufyMega = class extends EventEmitter14 {
28052
28264
  opts;
28053
28265
  mega;
28054
28266
  /**
@@ -28915,7 +29127,7 @@ var EufyMega = class extends EventEmitter13 {
28915
29127
  /**
28916
29128
  * The station a device's commands go through: the device itself, or the HomeBase the registry links it
28917
29129
  * to. Answers the target record, the station record and serial, the station's `device_type`, and the
28918
- * `t9000` / `t8n00` family flags.
29130
+ * `t9000` / `t8n00` / `t8223` family flags.
28919
29131
  */
28920
29132
  stationOf(sn) {
28921
29133
  const devices = this.registry.list();
@@ -28925,7 +29137,16 @@ var EufyMega = class extends EventEmitter13 {
28925
29137
  const raw = station?.raw ?? {};
28926
29138
  const deviceType = typeof raw.device_type === "number" ? raw.device_type : void 0;
28927
29139
  const family = { deviceType, model: station?.model };
28928
- 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
+ };
28929
29150
  }
28930
29151
  /** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
28931
29152
  stationDeviceTypeOf(sn) {
@@ -28984,7 +29205,8 @@ var EufyMega = class extends EventEmitter13 {
28984
29205
  * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
28985
29206
  * station and the devices attached to it go over RTC, with an attached device's command refused
28986
29207
  * unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
28987
- * 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.
28988
29210
  */
28989
29211
  routeCommand(sn, cmd) {
28990
29212
  if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
@@ -29013,25 +29235,32 @@ var EufyMega = class extends EventEmitter13 {
29013
29235
  return this.p2p.dispatchCommand(sn, cmd);
29014
29236
  }
29015
29237
  /**
29016
- * The RTC route for a T9000 or T8N00 station or a device attached to one, decided by the station's model
29017
- * classification; `undefined` for any other device. A T8N00 route carries its native `call` signalling. The admin id is the station's
29018
- * `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.
29019
29242
  */
29020
29243
  rtcRouteFor(sn) {
29021
- const { target, station, stationSn, raw, t9000, t8n00: native } = this.stationOf(sn);
29022
- 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))
29023
29247
  return void 0;
29024
29248
  const member = raw.member?.admin_user_id;
29025
29249
  const adminUserId = typeof member === "string" && member ? member : void 0;
29026
29250
  const base = { stationSn, adminUserId, attached: stationSn !== sn };
29251
+ if (doorbell)
29252
+ return { ...base, signalingMode: "call", iceTransportPolicy: "all", mediaTrack: true };
29027
29253
  return native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base;
29028
29254
  }
29029
29255
  /**
29030
- * The RTC media provider for a camera attached to a T9000 whose `device_channel` resolves back to it on
29031
- * 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.
29032
29259
  */
29033
29260
  rtcMediaFor(sn) {
29034
29261
  const route = this.rtcRouteFor(sn);
29262
+ if (route?.mediaTrack)
29263
+ return this.rtc.trackMediaProviderFor(route);
29035
29264
  if (!route?.attached || route.signalingMode === "call")
29036
29265
  return void 0;
29037
29266
  const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
@@ -30672,7 +30901,7 @@ var SolixClient = class _SolixClient {
30672
30901
  };
30673
30902
 
30674
30903
  // dist/transport/mqtt/solix-mqtt.js
30675
- import { EventEmitter as EventEmitter14 } from "node:events";
30904
+ import { EventEmitter as EventEmitter15 } from "node:events";
30676
30905
  var SOLIX_METER_FIELD_NAMES = {
30677
30906
  168: "meterPowerL1",
30678
30907
  // Anker Modbus: primary_phase_1_active_power
@@ -30850,7 +31079,7 @@ function addSolarbankScalars(frame, out) {
30850
31079
  out.gridExportLimit = df.readUInt16LE(5);
30851
31080
  }
30852
31081
  }
30853
- var SolixMqtt = class extends EventEmitter14 {
31082
+ var SolixMqtt = class extends EventEmitter15 {
30854
31083
  transport;
30855
31084
  appName;
30856
31085
  userId;