@mega-yfue/eufy-sdk 0.5.0-beta.72 → 0.5.0-beta.74

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.
@@ -456,8 +456,7 @@ export declare class EufyMega extends EventEmitter {
456
456
  private rtcRouteFor;
457
457
  /**
458
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.
459
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, unless an NVR camera has its own dedicated media call.
461
460
  */
462
461
  private rtcMediaFor;
463
462
  /**
package/dist/index.js CHANGED
@@ -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 isNvrMediaCamera = (ctx) => ctx.model === "T8E00" || ctx.model === "T8P00";
3658
3659
  var isDoorbellT8223 = (ctx) => ctx.model === "T8223";
3659
3660
  var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
3660
3661
  var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
@@ -4235,6 +4236,10 @@ function decodeAiDetectType(value) {
4235
4236
  pet: !!(value & AiDetectType.pet)
4236
4237
  };
4237
4238
  }
4239
+ function decodeAiDetectTypeParam(raw) {
4240
+ const value = typeof raw === "object" && raw !== null ? raw.ai_detect_type : raw;
4241
+ return typeof value === "number" && Number.isInteger(value) ? value : void 0;
4242
+ }
4238
4243
  function snoozePayload(seconds) {
4239
4244
  if (seconds <= 0)
4240
4245
  return { snooze_time: 0 };
@@ -4331,11 +4336,15 @@ var MOTION_MEMBERS = {
4331
4336
  * 1298 holds the detailed AI-type BITMASK (live-observed: `0x30000` enabled-base | type bits, e.g. a
4332
4337
  * T8425 reads `0x3000f`). Cams also report 1299 (`hbAiDetectType`) but that is a separate, simpler
4333
4338
  * value (1) — NOT this bitmask, so 1298 is the read/write id.
4339
+ *
4340
+ * The value arrives in two shapes, both read by the app's 1298 parser: a bare bitmask, or a JSON
4341
+ * object carrying the same bitmask as `ai_detect_type` (see {@link decodeAiDetectTypeParam}).
4334
4342
  */
4335
4343
  aiDetectType: {
4336
4344
  param: MOTION_CMD.AI_DETECT_TYPE,
4337
- type: "number",
4338
- kind: "bitfield",
4345
+ type: "string",
4346
+ decode: decodeAiDetectTypeParam,
4347
+ decodedKind: "bitfield",
4339
4348
  provenance: "verified",
4340
4349
  description: "AI detection type bitmask \u2014 which classes trigger detection. \u2705 Bits decoded live + confirmed vs the app: 0x30000 = enabled base, bit0 = human recognition, bit1 = human detection, bit2 = vehicle, bit3 = pet (see AiDetectType / encodeAiDetectType). \u2705 WRITE HW-verified live on T8124 (1350 SET_PAYLOAD, {ai_detect_type, channel}): each write landed byte-exact + read back (0x30003 \u2192 0x8 \u2192 0x3000b).",
4341
4350
  write: (v, ctx) => {
@@ -12336,10 +12345,13 @@ var SECURITY_PARAMS = {
12336
12345
  type: "string",
12337
12346
  provenance: "apk"
12338
12347
  },
12348
+ // A camera answers a bare bitmask; a camera behind an NVR-class hub answers a JSON object carrying
12349
+ // it as `ai_detect_type` beside `ai_switch`, `ai_notification`, `ai_use_global` and `channel`.
12339
12350
  1298: {
12340
12351
  name: "aiDetectType",
12341
- type: "number",
12342
- provenance: "apk"
12352
+ type: "string",
12353
+ provenance: "apk",
12354
+ encoding: "json"
12343
12355
  },
12344
12356
  // Cameras and a HomeBase 3 answer a bare number; a HomeBase S1 Pro (T9000) answers a JSON envelope
12345
12357
  // (`{"ai_switch":1,"smart_detection":…,"devlist":[…]}`).
@@ -13219,7 +13231,7 @@ var CLEAN_PARAMS = {
13219
13231
  },
13220
13232
  155: {
13221
13233
  name: "direction",
13222
- type: "enum",
13234
+ type: "string",
13223
13235
  provenance: "mega"
13224
13236
  },
13225
13237
  156: {
@@ -18687,16 +18699,18 @@ function h265Geometry(sps) {
18687
18699
  };
18688
18700
  }
18689
18701
 
18690
- // dist/transport/p2p/live-stream.js
18691
- var CMD_VIDEO_FRAME2 = 1300;
18702
+ // dist/transport/p2p/audio-frame.js
18692
18703
  var CMD_AUDIO_FRAME2 = 1301;
18693
- var VIDEO_HEADER_LEN = 22;
18694
18704
  var AUDIO_HEADER_LEN = 16;
18695
18705
  var AUDIO_TYPE_OFFSET = 5;
18706
+ var AUDIO_CODECS = { 0: "aac-lc", 2: "g711a", 7: "aac-eld" };
18707
+
18708
+ // dist/transport/p2p/live-stream.js
18709
+ var CMD_VIDEO_FRAME2 = 1300;
18710
+ var VIDEO_HEADER_LEN = 22;
18696
18711
  var SC4 = Buffer.from([0, 0, 0, 1]);
18697
18712
  var SC3 = Buffer.from([0, 0, 1]);
18698
18713
  var AAC_ELD_AUDIO_SPECIFIC_CONFIG = Buffer.from("f8f03000", "hex");
18699
- var AUDIO_CODECS = { 0: "aac-lc", 2: "g711a", 7: "aac-eld" };
18700
18714
  var MAX_TRACED_DECODE_FAILURES = 3;
18701
18715
  var DEFAULT_KEEPALIVE_MS = 3e3;
18702
18716
  var LiveStream = class extends EventEmitter3 {
@@ -23412,6 +23426,8 @@ var RtcLiveStream = class extends EventEmitter7 {
23412
23426
  opts;
23413
23427
  running = false;
23414
23428
  startSegment = -1;
23429
+ onConnected = () => this.sendStart();
23430
+ onError = (error) => this.fail(error);
23415
23431
  keepalive;
23416
23432
  width = 0;
23417
23433
  height = 0;
@@ -23428,11 +23444,24 @@ var RtcLiveStream = class extends EventEmitter7 {
23428
23444
  start() {
23429
23445
  if (this.running)
23430
23446
  return this;
23431
- const { session, seg, accountId } = this.opts;
23447
+ const { session } = this.opts;
23432
23448
  this.running = true;
23449
+ this.startSegment = -1;
23433
23450
  session.on("mediaData", this.onMedia);
23434
23451
  session.on("commandData", this.onCommand);
23435
23452
  session.on("close", this.onClose);
23453
+ if (this.opts.nvr) {
23454
+ session.on("connected", this.onConnected);
23455
+ session.on("error", this.onError);
23456
+ session.connect().catch(this.onError);
23457
+ } else
23458
+ this.sendStart();
23459
+ return this;
23460
+ }
23461
+ sendStart() {
23462
+ if (!this.running || this.startSegment !== -1)
23463
+ return;
23464
+ const { session, seg, accountId, nvr } = this.opts;
23436
23465
  this.startSegment = seg.next();
23437
23466
  const sent2 = session.sendCommand(buildPortalPacket({
23438
23467
  commandId: PORTAL_CMD_SET_PAYLOAD,
@@ -23448,7 +23477,7 @@ var RtcLiveStream = class extends EventEmitter7 {
23448
23477
  camera_type: 0,
23449
23478
  key: "",
23450
23479
  msg_id: 116,
23451
- audio_chn: -1,
23480
+ audio_chn: nvr ? this.opts.channel : -1,
23452
23481
  streamtype: 2,
23453
23482
  stitch_mode: 1,
23454
23483
  chn_list: [{ index: 0, chn: this.opts.channel, sensor: 0, isUps: 0, isClicked: true }],
@@ -23460,7 +23489,7 @@ var RtcLiveStream = class extends EventEmitter7 {
23460
23489
  }));
23461
23490
  if (!sent2) {
23462
23491
  queueMicrotask(() => this.fail(new Error(`rtc live ${this.tag}: command channel not open`)));
23463
- return this;
23492
+ return;
23464
23493
  }
23465
23494
  this.keepalive = setInterval(() => {
23466
23495
  if (!session.sendRaw(PORTAL_KEEPALIVE))
@@ -23468,20 +23497,22 @@ var RtcLiveStream = class extends EventEmitter7 {
23468
23497
  }, PORTAL_KEEPALIVE_MS);
23469
23498
  this.keepalive.unref?.();
23470
23499
  this.opts.logger?.debug?.(`[rtc] ${this.tag} live start sent`);
23471
- return this;
23472
23500
  }
23473
23501
  stop() {
23474
23502
  if (!this.running)
23475
23503
  return;
23476
23504
  this.teardown();
23477
23505
  const { session, seg, accountId } = this.opts;
23478
- session.sendCommand(buildPortalPacket({
23479
- commandId: PORTAL_CMD_SET_PAYLOAD,
23480
- channel: PORTAL_STATION_CHANNEL,
23481
- segment: seg.next(),
23482
- isResponse: LIVE_STREAM_ID,
23483
- payload: { account_id: accountId, cmd: LIVE_STOP, payload: {} }
23484
- }));
23506
+ if (this.startSegment !== -1)
23507
+ session.sendCommand(buildPortalPacket({
23508
+ commandId: PORTAL_CMD_SET_PAYLOAD,
23509
+ channel: PORTAL_STATION_CHANNEL,
23510
+ segment: seg.next(),
23511
+ isResponse: LIVE_STREAM_ID,
23512
+ payload: { account_id: accountId, cmd: LIVE_STOP, payload: {} }
23513
+ }));
23514
+ if (this.opts.nvr)
23515
+ session.close();
23485
23516
  this.emit("stop");
23486
23517
  }
23487
23518
  teardown() {
@@ -23493,11 +23524,15 @@ var RtcLiveStream = class extends EventEmitter7 {
23493
23524
  session.off("mediaData", this.onMedia);
23494
23525
  session.off("commandData", this.onCommand);
23495
23526
  session.off("close", this.onClose);
23527
+ session.off("connected", this.onConnected);
23528
+ session.off("error", this.onError);
23496
23529
  }
23497
23530
  fail(err) {
23498
23531
  if (!this.running)
23499
23532
  return;
23500
23533
  this.teardown();
23534
+ if (this.opts.nvr)
23535
+ this.opts.session.close();
23501
23536
  this.emit("error", err);
23502
23537
  }
23503
23538
  /** The start's ACK: a non-zero result code ends the pull. */
@@ -23510,7 +23545,20 @@ var RtcLiveStream = class extends EventEmitter7 {
23510
23545
  }
23511
23546
  onMediaFrame(frame) {
23512
23547
  const h = parsePortalHeader(frame);
23513
- if (!h || h.commandId !== LIVE_MEDIA || h.channel !== LIVE_MEDIA_CHANNEL)
23548
+ if (!h || h.channel !== LIVE_MEDIA_CHANNEL)
23549
+ return;
23550
+ if (h.commandId === CMD_AUDIO_FRAME2 && this.opts.nvr) {
23551
+ if (h.paramLength !== frame.length - PORTAL_HEADER_LENGTH || h.paramLength <= AUDIO_HEADER_LEN)
23552
+ return;
23553
+ const body = frame.subarray(PORTAL_HEADER_LENGTH);
23554
+ const codec = AUDIO_CODECS[body[AUDIO_TYPE_OFFSET]];
23555
+ if (body.readUInt32LE(0) !== body.length - AUDIO_HEADER_LEN || codec !== "aac-lc" && codec !== "g711a")
23556
+ return;
23557
+ const audio = { codec, data: body.subarray(AUDIO_HEADER_LEN) };
23558
+ this.emit("audio", audio);
23559
+ return;
23560
+ }
23561
+ if (h.commandId !== LIVE_MEDIA)
23514
23562
  return;
23515
23563
  if (h.paramLength > frame.length - PORTAL_HEADER_LENGTH)
23516
23564
  return;
@@ -24771,38 +24819,54 @@ var RtcCommandRouter = class {
24771
24819
  * session. Recording has no wire here and is refused.
24772
24820
  */
24773
24821
  trackMediaProviderFor(route) {
24774
- const sn = route.stationSn;
24775
- const sourceFor = (hints = {}) => {
24776
- let source = this.callSources.get(sn);
24822
+ return this.callMediaProviderFor(route, route.stationSn, (session) => new RtcCallLiveStream(session, this.deps.logger), `record is not available for ${route.stationSn} over a portal call`, 45e3);
24823
+ }
24824
+ /** NVR media uses a dedicated call per camera, independent of station commands. */
24825
+ nvrMediaProviderFor(route) {
24826
+ return this.callMediaProviderFor(route, route.cameraSn, (session, accountId) => new RtcLiveStream({
24827
+ session,
24828
+ seg: new SegmentCounter(),
24829
+ stationSn: route.stationSn,
24830
+ channel: route.channel,
24831
+ accountId,
24832
+ logger: this.deps.logger,
24833
+ nvr: true
24834
+ }), "record is not available over an NVR portal call");
24835
+ }
24836
+ /** Share a dedicated-call source and its consumer operations under one route key. */
24837
+ callMediaProviderFor(route, key, makeStream, recordError, warmTimeoutMs) {
24838
+ const sourceFor = (opts = {}) => {
24839
+ opts.signal?.throwIfAborted();
24840
+ let source = this.callSources.get(key);
24777
24841
  if (!source) {
24778
24842
  source = new SharedLiveSource({
24779
24843
  makeStream: () => {
24780
24844
  const identity = this.deps.identity();
24781
24845
  if (!identity)
24782
- throw new Error(`rtc: not logged in, cannot call ${sn}`);
24783
- const session = this.newSession(route, route.adminUserId || identity.userId, identity);
24784
- return new RtcCallLiveStream(session, this.deps.logger);
24846
+ throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
24847
+ const accountId = route.adminUserId || identity.userId;
24848
+ return makeStream(this.newSession(route, accountId, identity), accountId);
24785
24849
  },
24786
- label: sn,
24850
+ label: key,
24787
24851
  logger: this.deps.logger,
24788
- powered: hints.powered,
24789
- preBufferSeconds: hints.preBufferSeconds,
24790
- warmTimeoutMs: 45e3
24852
+ powered: opts.powered,
24853
+ preBufferSeconds: opts.preBufferSeconds,
24854
+ warmTimeoutMs
24791
24855
  });
24792
- this.callSources.set(sn, source);
24856
+ this.callSources.set(key, source);
24793
24857
  }
24794
24858
  return source;
24795
24859
  };
24796
24860
  return {
24797
24861
  live: async (opts) => attachUnlessAborted(sourceFor(opts), opts?.signal),
24798
24862
  openReadable: async (opts) => openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts),
24799
- snapshotLive: (opts) => captureSnapshotFromShared(sourceFor(opts), {
24863
+ snapshotLive: async (opts) => captureSnapshotFromShared(sourceFor(opts), {
24800
24864
  ...opts,
24801
24865
  logger: this.deps.logger,
24802
24866
  ffmpegPath: this.deps.ffmpegPath
24803
24867
  }),
24804
24868
  record: async () => {
24805
- throw new Error(`record is not available for ${sn} over a portal call`);
24869
+ throw new Error(recordError);
24806
24870
  }
24807
24871
  };
24808
24872
  }
@@ -29435,19 +29499,24 @@ var EufyMega = class extends EventEmitter14 {
29435
29499
  }
29436
29500
  /**
29437
29501
  * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
29438
- * `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
29439
- * a T8N00, whose route carries commands only.
29502
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, unless an NVR camera has its own dedicated media call.
29440
29503
  */
29441
29504
  rtcMediaFor(sn) {
29442
29505
  const route = this.rtcRouteFor(sn);
29443
29506
  if (route?.mediaTrack)
29444
29507
  return this.rtc.trackMediaProviderFor(route);
29445
- if (!route?.attached || route.signalingMode === "call")
29508
+ if (!route?.attached)
29446
29509
  return void 0;
29447
- const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
29510
+ const camera = this.registry.list().find((d) => d.sn === sn);
29511
+ const raw = camera?.raw ?? {};
29448
29512
  const channel = typeof raw.device_channel === "number" ? raw.device_channel : void 0;
29449
29513
  if (channel === void 0 || this.registry.serialForFrame(route.stationSn, channel) !== sn)
29450
29514
  return void 0;
29515
+ if (route.signalingMode === "call") {
29516
+ if (!camera || !isNvrMediaCamera(camera))
29517
+ return void 0;
29518
+ return this.rtc.nvrMediaProviderFor({ ...route, cameraSn: sn, channel });
29519
+ }
29451
29520
  return this.rtc.mediaProviderFor({ ...route, cameraSn: sn, channel });
29452
29521
  }
29453
29522
  /**