@mega-yfue/eufy-sdk 0.5.0-beta.66 → 0.5.0-beta.68

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
 
@@ -13563,9 +13564,12 @@ function coerceByType(type, raw, name, logger = noopLogger) {
13563
13564
  return String(raw);
13564
13565
  }
13565
13566
  }
13567
+ var BOOL_WIRE = /* @__PURE__ */ new Set(["0", "1", "true", "false"]);
13566
13568
  function coerce(spec, raw, invert = false, logger = noopLogger) {
13567
13569
  if (spec.decode)
13568
13570
  return spec.decode(raw);
13571
+ if (spec.raw && spec.type === "bool" && typeof raw === "string" && !BOOL_WIRE.has(raw))
13572
+ return raw;
13569
13573
  const v = coerceByType(spec.type, raw, spec.name, spec.raw ? noopLogger : logger);
13570
13574
  return spec.type === "bool" && invert ? !v : v;
13571
13575
  }
@@ -23366,11 +23370,139 @@ var RtcLiveStream = class extends EventEmitter7 {
23366
23370
  }
23367
23371
  };
23368
23372
 
23373
+ // dist/transport/rtc/call-live.js
23374
+ import { EventEmitter as EventEmitter8 } from "node:events";
23375
+ var START_CODE = Buffer.from([0, 0, 0, 1]);
23376
+ var H264Depacketizer = class {
23377
+ onUnit;
23378
+ nals = [];
23379
+ fu;
23380
+ lastSeq = -1;
23381
+ broken = false;
23382
+ constructor(onUnit) {
23383
+ this.onUnit = onUnit;
23384
+ }
23385
+ push(packet) {
23386
+ if (packet.length < 12 || packet[0] >> 6 !== 2)
23387
+ return;
23388
+ const pt = packet[1] & 127;
23389
+ if (pt >= 72 && pt <= 79)
23390
+ return;
23391
+ const marker = (packet[1] & 128) !== 0;
23392
+ const seq = packet.readUInt16BE(2);
23393
+ const timestamp = packet.readUInt32BE(4);
23394
+ if (this.lastSeq >= 0 && seq !== (this.lastSeq + 1 & 65535))
23395
+ this.broken = true;
23396
+ this.lastSeq = seq;
23397
+ const cc = packet[0] & 15;
23398
+ let off = 12 + cc * 4;
23399
+ if (packet[0] & 16) {
23400
+ if (packet.length < off + 4)
23401
+ return;
23402
+ off += 4 + packet.readUInt16BE(off + 2) * 4;
23403
+ }
23404
+ let end = packet.length;
23405
+ if (packet[0] & 32)
23406
+ end -= packet[packet.length - 1];
23407
+ if (end <= off)
23408
+ return;
23409
+ const p = packet.subarray(off, end);
23410
+ const type = p[0] & 31;
23411
+ if (type >= 1 && type <= 23) {
23412
+ this.nals.push(Buffer.from(p));
23413
+ } else if (type === 24) {
23414
+ let o = 1;
23415
+ while (o + 2 <= p.length) {
23416
+ const len = p.readUInt16BE(o);
23417
+ if (o + 2 + len > p.length)
23418
+ break;
23419
+ this.nals.push(Buffer.from(p.subarray(o + 2, o + 2 + len)));
23420
+ o += 2 + len;
23421
+ }
23422
+ } else if (type === 28 && p.length > 2) {
23423
+ const start = (p[1] & 128) !== 0;
23424
+ const stop = (p[1] & 64) !== 0;
23425
+ if (start)
23426
+ this.fu = [Buffer.from([p[0] & 224 | p[1] & 31])];
23427
+ if (this.fu)
23428
+ this.fu.push(Buffer.from(p.subarray(2)));
23429
+ else
23430
+ this.broken = true;
23431
+ if (stop && this.fu) {
23432
+ this.nals.push(Buffer.concat(this.fu));
23433
+ this.fu = void 0;
23434
+ }
23435
+ }
23436
+ if (marker) {
23437
+ if (!this.broken && !this.fu && this.nals.length) {
23438
+ const parts = [];
23439
+ for (const n of this.nals)
23440
+ parts.push(START_CODE, n);
23441
+ this.onUnit(Buffer.concat(parts), timestamp);
23442
+ }
23443
+ this.nals = [];
23444
+ this.fu = void 0;
23445
+ this.broken = false;
23446
+ }
23447
+ }
23448
+ };
23449
+ var RtcCallLiveStream = class extends EventEmitter8 {
23450
+ session;
23451
+ logger;
23452
+ running = false;
23453
+ depacketizer = new H264Depacketizer((unit, ts) => this.onUnit(unit, ts));
23454
+ /** `session` is a session on a {@link RtcPeerOptions.mediaTrack} peer, not yet connected. */
23455
+ constructor(session, logger) {
23456
+ super();
23457
+ this.session = session;
23458
+ this.logger = logger ?? noopLogger;
23459
+ }
23460
+ start() {
23461
+ if (this.running)
23462
+ return this;
23463
+ this.running = true;
23464
+ const { session } = this;
23465
+ session.on("rtp", (packet) => this.depacketizer.push(packet));
23466
+ session.on("close", () => this.fail(new Error("rtc call: session closed")));
23467
+ session.on("error", (e) => this.fail(e));
23468
+ session.connect().catch((e) => this.fail(e instanceof Error ? e : new Error(String(e))));
23469
+ return this;
23470
+ }
23471
+ stop() {
23472
+ if (!this.running)
23473
+ return;
23474
+ this.teardown();
23475
+ this.emit("stop");
23476
+ }
23477
+ onUnit(unit, timestamp) {
23478
+ const frame = {
23479
+ codec: "h264",
23480
+ data: unit,
23481
+ keyframe: hasIdr(unit, "h264"),
23482
+ width: 0,
23483
+ height: 0,
23484
+ sourceTimestamp: timestamp
23485
+ };
23486
+ this.emit("video", frame);
23487
+ }
23488
+ fail(err) {
23489
+ if (!this.running)
23490
+ return;
23491
+ this.logger.warn(`[rtc-call] ${err.message}`);
23492
+ this.teardown();
23493
+ this.emit("error", err);
23494
+ }
23495
+ teardown() {
23496
+ this.running = false;
23497
+ this.session.close();
23498
+ }
23499
+ };
23500
+
23369
23501
  // dist/transport/rtc/session.js
23370
- import { EventEmitter as EventEmitter10 } from "node:events";
23502
+ import { EventEmitter as EventEmitter11 } from "node:events";
23371
23503
 
23372
23504
  // dist/transport/rtc/peer.js
23373
- import { EventEmitter as EventEmitter8 } from "node:events";
23505
+ import { EventEmitter as EventEmitter9 } from "node:events";
23374
23506
 
23375
23507
  // dist/transport/rtc/ptcs-framer.js
23376
23508
  var MAGIC3 = Buffer.from("PTCS", "ascii");
@@ -23621,6 +23753,17 @@ function forceDtlsRole(sdp, role) {
23621
23753
  function toWireCandidate(candidate) {
23622
23754
  return candidate.trim().replace(/^a=/, "");
23623
23755
  }
23756
+ function midOf(sdp, kind) {
23757
+ const section = sdp.split(/(?=^m=)/m).find((s) => s.startsWith(`m=${kind} `));
23758
+ return section?.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim();
23759
+ }
23760
+ function rejectDataSection(sdp) {
23761
+ const appMid = midOf(sdp, "application");
23762
+ const out = sdp.replace(/^m=application \d+/m, "m=application 0");
23763
+ if (appMid === void 0)
23764
+ return out;
23765
+ return out.replace(/^(a=group:BUNDLE)([^\r\n]*)/m, (_, head, mids) => [head, ...mids.split(" ").filter((t) => t && t !== appMid)].join(" "));
23766
+ }
23624
23767
 
23625
23768
  // dist/transport/rtc/peer.js
23626
23769
  var DATA_CHANNEL_LABELS = ["WebrtcDataChannel", "audio", "idr", "video", "notify", "download"];
@@ -23655,7 +23798,7 @@ async function loadNativePeerFactory() {
23655
23798
  return (name, config) => new ndc.PeerConnection(name, config);
23656
23799
  }
23657
23800
  var ANSWER_TIMEOUT_MS = 15e3;
23658
- var RtcPeer = class extends EventEmitter8 {
23801
+ var RtcPeer = class extends EventEmitter9 {
23659
23802
  opts;
23660
23803
  pc;
23661
23804
  channels = /* @__PURE__ */ new Map();
@@ -23666,6 +23809,8 @@ var RtcPeer = class extends EventEmitter8 {
23666
23809
  remoteMid = HUB_SDP_MID;
23667
23810
  handlingOffer = false;
23668
23811
  channelsCreated = false;
23812
+ videoMid;
23813
+ videoTrack;
23669
23814
  gatheringDone = false;
23670
23815
  /** Set by the framer's wire callback when the native channel refused a packet mid-send. */
23671
23816
  wireSendFailed = false;
@@ -23715,6 +23860,8 @@ var RtcPeer = class extends EventEmitter8 {
23715
23860
  this.emit("connectionState", state);
23716
23861
  });
23717
23862
  pc.onDataChannel((dc) => this.wireChannel(dc.getLabel(), dc));
23863
+ if (this.opts.mediaTrack)
23864
+ pc.onTrack((track) => this.wireVideoTrack(track));
23718
23865
  }
23719
23866
  /**
23720
23867
  * The hub offered: apply the offer, declare the channels, return the answer SDP to signal back.
@@ -23724,7 +23871,9 @@ var RtcPeer = class extends EventEmitter8 {
23724
23871
  * only what is announced. The offer goes in before the channels are declared: `createDataChannel` on
23725
23872
  * a peer without a remote description starts its own negotiation and leaves it in `have-local-offer`.
23726
23873
  * 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.
23874
+ * is taken as the local description: one read in `have-local-offer` is an offer. A
23875
+ * {@link RtcPeerOptions.mediaTrack} peer applies the offer with its application section rejected,
23876
+ * declares no channels, and adds remote candidates on the offer's first (bundle-tagged) mid.
23728
23877
  */
23729
23878
  async handleRemoteOffer(offerSdp) {
23730
23879
  const pc = this.pc;
@@ -23734,9 +23883,12 @@ var RtcPeer = class extends EventEmitter8 {
23734
23883
  throw new Error("RTC peer already handling an offer");
23735
23884
  this.handlingOffer = true;
23736
23885
  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;
23886
+ const media = this.opts.mediaTrack === true;
23887
+ const pinned = forceDtlsRole(offerSdp, "passive");
23888
+ const offer = media ? rejectDataSection(pinned) : pinned;
23889
+ this.remoteMid = media ? offer.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim() ?? HUB_SDP_MID : midOf(offer, "application") ?? HUB_SDP_MID;
23890
+ if (media)
23891
+ this.videoMid = midOf(offer, "video");
23740
23892
  const answerWait = new Promise((resolve, reject) => {
23741
23893
  const timer = setTimeout(() => {
23742
23894
  this.localAnswer = void 0;
@@ -23746,7 +23898,8 @@ var RtcPeer = class extends EventEmitter8 {
23746
23898
  });
23747
23899
  pc.setRemoteDescription(offer, "offer");
23748
23900
  this.remoteSet = true;
23749
- this.createChannels();
23901
+ if (!media)
23902
+ this.createChannels();
23750
23903
  const local = pc.localDescription();
23751
23904
  let answer = String(local?.type ?? "").toLowerCase() === "answer" ? local?.sdp ?? "" : "";
23752
23905
  if (!answer)
@@ -23815,6 +23968,7 @@ var RtcPeer = class extends EventEmitter8 {
23815
23968
  this.pc = void 0;
23816
23969
  this.channels.clear();
23817
23970
  this.channelsCreated = false;
23971
+ this.videoTrack = void 0;
23818
23972
  this.remoteSet = false;
23819
23973
  this.pending.length = 0;
23820
23974
  }
@@ -23878,6 +24032,14 @@ var RtcPeer = class extends EventEmitter8 {
23878
24032
  this.framer?.recvPacket(buf);
23879
24033
  });
23880
24034
  }
24035
+ /** Take the offer's video m-line as the one track whose RTP is emitted; any other track is left alone. */
24036
+ wireVideoTrack(track) {
24037
+ if (this.videoTrack || this.videoMid === void 0 || track.mid() !== this.videoMid)
24038
+ return;
24039
+ this.videoTrack = track;
24040
+ this.logger.debug(`[rtc] video track ${this.videoMid}`);
24041
+ track.onMessage((msg) => this.emit("rtp", Buffer.isBuffer(msg) ? msg : Buffer.from(msg)));
24042
+ }
23881
24043
  addNow(candidate) {
23882
24044
  try {
23883
24045
  this.pc?.addRemoteCandidate(candidate, this.remoteMid);
@@ -23893,7 +24055,7 @@ var RtcPeer = class extends EventEmitter8 {
23893
24055
  };
23894
24056
 
23895
24057
  // dist/transport/rtc/signaling.js
23896
- import { EventEmitter as EventEmitter9 } from "node:events";
24058
+ import { EventEmitter as EventEmitter10 } from "node:events";
23897
24059
  import { createHmac as createHmac3, randomUUID as randomUUID2 } from "node:crypto";
23898
24060
  var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
23899
24061
  var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
@@ -23909,7 +24071,7 @@ var defaultSocket = (url, init) => new WebSocket(url, {
23909
24071
  protocols: init.protocols,
23910
24072
  headers: { Origin: init.origin }
23911
24073
  });
23912
- var RtcSignalingClient = class extends EventEmitter9 {
24074
+ var RtcSignalingClient = class extends EventEmitter10 {
23913
24075
  opts;
23914
24076
  ws;
23915
24077
  sign;
@@ -24134,7 +24296,7 @@ var RtcSignalingClient = class extends EventEmitter9 {
24134
24296
 
24135
24297
  // dist/transport/rtc/session.js
24136
24298
  var AUTH_TIMEOUT_MS = 15e3;
24137
- var RtcSession = class extends EventEmitter10 {
24299
+ var RtcSession = class extends EventEmitter11 {
24138
24300
  opts;
24139
24301
  signaling;
24140
24302
  peer;
@@ -24153,6 +24315,7 @@ var RtcSession = class extends EventEmitter10 {
24153
24315
  this.logger = opts.logger ?? noopLogger;
24154
24316
  this.signaling = (opts.createSignaling ?? ((o) => new RtcSignalingClient(o)))(opts);
24155
24317
  this.peer = (opts.createPeer ?? ((o) => new RtcPeer(o)))({ logger: this.logger, ...opts.peer });
24318
+ const mediaTrack = opts.peer?.mediaTrack === true;
24156
24319
  this.signaling.on("message", (inner) => {
24157
24320
  this.chain = this.chain.then(() => this.onSignaling(inner)).catch((e) => {
24158
24321
  this.emit("error", e instanceof Error ? e : new Error(String(e)));
@@ -24180,13 +24343,14 @@ var RtcSession = class extends EventEmitter10 {
24180
24343
  this.announceClose();
24181
24344
  });
24182
24345
  this.peer.on("connectionState", (state) => {
24183
- if ((state === "failed" || state === "closed") && this.connected) {
24346
+ if ((state === "failed" || state === "closed") && (this.connected || mediaTrack)) {
24184
24347
  this.connected = false;
24185
24348
  this.announceClose();
24186
24349
  }
24187
24350
  });
24188
24351
  this.peer.on("error", (e) => this.emit("error", e));
24189
24352
  this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
24353
+ this.peer.on("rtp", (packet) => this.emit("rtp", packet));
24190
24354
  this.peer.on("data", (frame, linkType) => {
24191
24355
  if (linkType === PortalLinkType.LIVE)
24192
24356
  this.emit("mediaData", frame);
@@ -24383,6 +24547,7 @@ var RtcCommandRouter = class {
24383
24547
  sessions = /* @__PURE__ */ new Map();
24384
24548
  lives = /* @__PURE__ */ new Map();
24385
24549
  warm = /* @__PURE__ */ new Map();
24550
+ callSources = /* @__PURE__ */ new Map();
24386
24551
  constructor(deps) {
24387
24552
  this.deps = deps;
24388
24553
  }
@@ -24390,6 +24555,8 @@ var RtcCommandRouter = class {
24390
24555
  if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
24391
24556
  throw new Error(`rtc: ${cmd.kind} is not routable over the RTC control channel (only set-payload or set-json)`);
24392
24557
  }
24558
+ if (route.mediaTrack)
24559
+ throw new Error(`rtc: ${route.stationSn} serves media only and takes no portal command`);
24393
24560
  const identity = this.deps.identity();
24394
24561
  if (!identity)
24395
24562
  throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
@@ -24424,11 +24591,56 @@ var RtcCommandRouter = class {
24424
24591
  }
24425
24592
  };
24426
24593
  }
24594
+ /**
24595
+ * Live view of a station on a {@link RtcRoute.mediaTrack} route: a live feed, a readable feed and a live
24596
+ * still, from one shared source per station whose every warm-up places a fresh `call` on its own
24597
+ * session. Recording has no wire here and is refused.
24598
+ */
24599
+ trackMediaProviderFor(route) {
24600
+ const sn = route.stationSn;
24601
+ const sourceFor = (hints = {}) => {
24602
+ let source = this.callSources.get(sn);
24603
+ if (!source) {
24604
+ source = new SharedLiveSource({
24605
+ makeStream: () => {
24606
+ const identity = this.deps.identity();
24607
+ if (!identity)
24608
+ throw new Error(`rtc: not logged in, cannot call ${sn}`);
24609
+ const session = this.newSession(route, route.adminUserId || identity.userId, identity);
24610
+ return new RtcCallLiveStream(session, this.deps.logger);
24611
+ },
24612
+ label: sn,
24613
+ logger: this.deps.logger,
24614
+ powered: hints.powered,
24615
+ preBufferSeconds: hints.preBufferSeconds,
24616
+ warmTimeoutMs: 45e3
24617
+ });
24618
+ this.callSources.set(sn, source);
24619
+ }
24620
+ return source;
24621
+ };
24622
+ return {
24623
+ live: async (opts) => attachUnlessAborted(sourceFor(opts), opts?.signal),
24624
+ openReadable: async (opts) => openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts),
24625
+ snapshotLive: (opts) => captureSnapshotFromShared(sourceFor(opts), {
24626
+ ...opts,
24627
+ logger: this.deps.logger,
24628
+ ffmpegPath: this.deps.ffmpegPath
24629
+ }),
24630
+ record: async () => {
24631
+ throw new Error(`record is not available for ${sn} over a portal call`);
24632
+ }
24633
+ };
24634
+ }
24427
24635
  /**
24428
24636
  * Tear down every station session (logout / shutdown). Each live source is disposed before its session
24429
24637
  * closes, so its consumers end cleanly and its stop still rides the session.
24430
24638
  */
24431
24639
  close() {
24640
+ for (const [sn, src] of this.callSources) {
24641
+ this.callSources.delete(sn);
24642
+ src.dispose();
24643
+ }
24432
24644
  for (const [sn, live] of this.lives) {
24433
24645
  this.lives.delete(sn);
24434
24646
  live.source.dispose();
@@ -24558,6 +24770,19 @@ var RtcCommandRouter = class {
24558
24770
  }
24559
24771
  });
24560
24772
  }
24773
+ newSession(route, adminUserId, identity) {
24774
+ return (this.deps.createSession ?? ((o) => new RtcSession(o)))({
24775
+ authToken: identity.authToken,
24776
+ gtoken: identity.gtoken,
24777
+ stationSn: route.stationSn,
24778
+ adminUserId,
24779
+ shard: this.deps.shard(),
24780
+ country: this.deps.country ?? "US",
24781
+ logger: this.deps.logger,
24782
+ signalingMode: route.signalingMode,
24783
+ peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy, mediaTrack: route.mediaTrack }
24784
+ });
24785
+ }
24561
24786
  async stationSession(route, adminUserId, identity) {
24562
24787
  const sn = route.stationSn;
24563
24788
  const existing = this.sessions.get(sn);
@@ -24569,17 +24794,7 @@ var RtcCommandRouter = class {
24569
24794
  }
24570
24795
  this.drop(sn, existing);
24571
24796
  }
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
- });
24797
+ const session = this.newSession(route, adminUserId, identity);
24583
24798
  const ready = this.bringUp(sn, session);
24584
24799
  const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
24585
24800
  session.on("error", (e) => this.deps.onError?.(e));
@@ -26390,7 +26605,7 @@ async function resolveLightEffect(mega, lightId) {
26390
26605
  }
26391
26606
 
26392
26607
  // dist/transport/push/push-client.js
26393
- import { EventEmitter as EventEmitter12 } from "node:events";
26608
+ import { EventEmitter as EventEmitter13 } from "node:events";
26394
26609
  import tls2 from "node:tls";
26395
26610
 
26396
26611
  // dist/transport/push/proto.js
@@ -26645,7 +26860,7 @@ var MessageTag;
26645
26860
  })(MessageTag || (MessageTag = {}));
26646
26861
 
26647
26862
  // dist/transport/push/parser.js
26648
- import { EventEmitter as EventEmitter11 } from "node:events";
26863
+ import { EventEmitter as EventEmitter12 } from "node:events";
26649
26864
  var State;
26650
26865
  (function(State2) {
26651
26866
  State2[State2["VERSION_TAG_AND_SIZE"] = 0] = "VERSION_TAG_AND_SIZE";
@@ -26663,7 +26878,7 @@ var TAG_TO_TYPE = {
26663
26878
  [MessageTag.DataMessageStanza]: "mcs_proto.DataMessageStanza",
26664
26879
  [MessageTag.StreamErrorStanza]: "mcs_proto.StreamErrorStanza"
26665
26880
  };
26666
- var McsParser = class extends EventEmitter11 {
26881
+ var McsParser = class extends EventEmitter12 {
26667
26882
  data = Buffer.alloc(0);
26668
26883
  state = State.VERSION_TAG_AND_SIZE;
26669
26884
  messageTag = 0;
@@ -26841,7 +27056,7 @@ function normalizePushEvent(raw) {
26841
27056
  raw
26842
27057
  };
26843
27058
  }
26844
- var PushClient = class _PushClient extends EventEmitter12 {
27059
+ var PushClient = class _PushClient extends EventEmitter13 {
26845
27060
  creds;
26846
27061
  logger;
26847
27062
  /** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
@@ -28048,7 +28263,7 @@ function readinessSnapshot(readiness, state = readiness.state) {
28048
28263
  wiredP2p: Object.freeze({ ...readiness.wiredP2p })
28049
28264
  });
28050
28265
  }
28051
- var EufyMega = class extends EventEmitter13 {
28266
+ var EufyMega = class extends EventEmitter14 {
28052
28267
  opts;
28053
28268
  mega;
28054
28269
  /**
@@ -28915,7 +29130,7 @@ var EufyMega = class extends EventEmitter13 {
28915
29130
  /**
28916
29131
  * The station a device's commands go through: the device itself, or the HomeBase the registry links it
28917
29132
  * to. Answers the target record, the station record and serial, the station's `device_type`, and the
28918
- * `t9000` / `t8n00` family flags.
29133
+ * `t9000` / `t8n00` / `t8223` family flags.
28919
29134
  */
28920
29135
  stationOf(sn) {
28921
29136
  const devices = this.registry.list();
@@ -28925,7 +29140,16 @@ var EufyMega = class extends EventEmitter13 {
28925
29140
  const raw = station?.raw ?? {};
28926
29141
  const deviceType = typeof raw.device_type === "number" ? raw.device_type : void 0;
28927
29142
  const family = { deviceType, model: station?.model };
28928
- return { target, station, stationSn, raw, deviceType, t9000: isStation9000(family), t8n00: isNvrT8N00(family) };
29143
+ return {
29144
+ target,
29145
+ station,
29146
+ stationSn,
29147
+ raw,
29148
+ deviceType,
29149
+ t9000: isStation9000(family),
29150
+ t8n00: isNvrT8N00(family),
29151
+ t8223: isDoorbellT8223(family)
29152
+ };
28929
29153
  }
28930
29154
  /** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
28931
29155
  stationDeviceTypeOf(sn) {
@@ -28984,7 +29208,8 @@ var EufyMega = class extends EventEmitter13 {
28984
29208
  * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
28985
29209
  * station and the devices attached to it go over RTC, with an attached device's command refused
28986
29210
  * 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.
29211
+ * relay candidates; T9000 retains compact SDP and relay-only candidates. A T8223 doorbell's route serves
29212
+ * media only, and the RTC router refuses a command on it. Everything else is P2P.
28988
29213
  */
28989
29214
  routeCommand(sn, cmd) {
28990
29215
  if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
@@ -29013,25 +29238,32 @@ var EufyMega = class extends EventEmitter13 {
29013
29238
  return this.p2p.dispatchCommand(sn, cmd);
29014
29239
  }
29015
29240
  /**
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.
29241
+ * The RTC route for a T9000 or T8N00 station or a device attached to one, or a T8223 doorbell that is its
29242
+ * own station, decided by the station's model classification; `undefined` for any other device. A T8N00
29243
+ * route carries its native `call` signalling; a T8223 route does too, and streams its video on a media
29244
+ * track. The admin id is the station's `member.admin_user_id`, else the logged-in user.
29019
29245
  */
29020
29246
  rtcRouteFor(sn) {
29021
- const { target, station, stationSn, raw, t9000, t8n00: native } = this.stationOf(sn);
29022
- if (!target || !station || !(t9000 || native))
29247
+ const { target, station, stationSn, raw, t9000, t8n00: native, t8223 } = this.stationOf(sn);
29248
+ const doorbell = t8223 && stationSn === sn;
29249
+ if (!target || !station || !(t9000 || native || doorbell))
29023
29250
  return void 0;
29024
29251
  const member = raw.member?.admin_user_id;
29025
29252
  const adminUserId = typeof member === "string" && member ? member : void 0;
29026
29253
  const base = { stationSn, adminUserId, attached: stationSn !== sn };
29254
+ if (doorbell)
29255
+ return { ...base, signalingMode: "call", iceTransportPolicy: "all", mediaTrack: true };
29027
29256
  return native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base;
29028
29257
  }
29029
29258
  /**
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.
29259
+ * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
29260
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
29261
+ * a T8N00, whose route carries commands only.
29032
29262
  */
29033
29263
  rtcMediaFor(sn) {
29034
29264
  const route = this.rtcRouteFor(sn);
29265
+ if (route?.mediaTrack)
29266
+ return this.rtc.trackMediaProviderFor(route);
29035
29267
  if (!route?.attached || route.signalingMode === "call")
29036
29268
  return void 0;
29037
29269
  const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
@@ -30672,7 +30904,7 @@ var SolixClient = class _SolixClient {
30672
30904
  };
30673
30905
 
30674
30906
  // dist/transport/mqtt/solix-mqtt.js
30675
- import { EventEmitter as EventEmitter14 } from "node:events";
30907
+ import { EventEmitter as EventEmitter15 } from "node:events";
30676
30908
  var SOLIX_METER_FIELD_NAMES = {
30677
30909
  168: "meterPowerL1",
30678
30910
  // Anker Modbus: primary_phase_1_active_power
@@ -30850,7 +31082,7 @@ function addSolarbankScalars(frame, out) {
30850
31082
  out.gridExportLimit = df.readUInt16LE(5);
30851
31083
  }
30852
31084
  }
30853
- var SolixMqtt = class extends EventEmitter14 {
31085
+ var SolixMqtt = class extends EventEmitter15 {
30854
31086
  transport;
30855
31087
  appName;
30856
31088
  userId;