@mega-yfue/eufy-sdk 0.5.0-beta.73 → 0.5.0-beta.75

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";
@@ -5618,6 +5619,7 @@ var CAMERA_MEMBERS = {
5618
5619
  type: "bool",
5619
5620
  kind: "boolean",
5620
5621
  provenance: "verified",
5622
+ available: isCameraCodec,
5621
5623
  invert: true,
5622
5624
  invertFor: (ctx) => isT8410HomeBase3(ctx) ? false : void 0,
5623
5625
  readAliases: [{ paramType: 2001, invert: false }],
@@ -5642,6 +5644,7 @@ var CAMERA_MEMBERS = {
5642
5644
  type: "bool",
5643
5645
  kind: "boolean",
5644
5646
  provenance: "apk",
5647
+ available: isCameraCodec,
5645
5648
  description: "Rotate the image 180\xB0 (ROTATE_IMAGE 1207): false = normal, true = flipped. For ceiling/upside-down mounts. Write is a direct param; wire from the app parser (unverified live).",
5646
5649
  write: (v, ctx) => setScalar(CAMERA_CMD.ROTATE_IMAGE, asBool(v) ? 1 : 0, ctx, "auto")
5647
5650
  },
@@ -5657,6 +5660,7 @@ var CAMERA_MEMBERS = {
5657
5660
  kind: "enum",
5658
5661
  enumValues: { 0: "Off", 1: "Timestamp", 2: "Timestamp + Logo" },
5659
5662
  provenance: "verified",
5663
+ available: isCameraCodec,
5660
5664
  description: "On-screen watermark/OSD overlay (CMD_SET_DEVS_OSD 1214). \u2705 wire verified live (T8425 ch3): direct-binary [ch][value 0/1/2] \u2014 a 3-value enum, not a bool. Enum labels are best-guess.",
5661
5665
  write: (v, ctx) => {
5662
5666
  const w = coerceEnumValue(Watermark, v);
@@ -5673,6 +5677,7 @@ var CAMERA_MEMBERS = {
5673
5677
  type: "bool",
5674
5678
  kind: "boolean",
5675
5679
  provenance: "verified",
5680
+ available: isCameraCodec,
5676
5681
  description: "Trigger on sound as well as motion (SOUND_DETECTION 6043). \u2705 Write confirmed live on a T8410 (mains, standalone, own channel): both directions sent through this member and read back on the cloud param, and the app's own frame captured byte-exact for the same body.",
5677
5682
  write: (v, ctx) => setJson(CAMERA_CMD.SOUND_DETECTION, { status: asBool(v) ? 1 : 0 }, ctx)
5678
5683
  },
@@ -5688,6 +5693,7 @@ var CAMERA_MEMBERS = {
5688
5693
  type: "number",
5689
5694
  kind: "scalar",
5690
5695
  provenance: "verified",
5696
+ available: isCameraCodec,
5691
5697
  description: "Sound-detection sensitivity: 1 = lowest, 3 = mid, 5 = highest (SOUND_DETECTION_SENSITIVITY 6044). \u2705 Write confirmed live on a T8410: 1 and 5 sent consecutively through this member and read back on the cloud param. 2 and 4 are the scale's shape, not observed values.",
5692
5698
  write: (v, ctx) => {
5693
5699
  const n = Number(v);
@@ -5707,6 +5713,7 @@ var CAMERA_MEMBERS = {
5707
5713
  kind: "enum",
5708
5714
  enumValues: { 1: "Crying", 2: "All Sound" },
5709
5715
  provenance: "verified",
5716
+ available: isCameraCodec,
5710
5717
  description: "Which sounds trigger a detection: crying alone, or any sound (SOUND_DETECTION_TYPE 6046). \u2705 Write confirmed live on a T8410: both values sent consecutively through this member and read back on the cloud param.",
5711
5718
  write: (v, ctx) => {
5712
5719
  const type = coerceEnumValue(SoundDetectionType, v);
@@ -5727,6 +5734,7 @@ var CAMERA_MEMBERS = {
5727
5734
  kind: "enum",
5728
5735
  enumValues: { 1: "Text Only", 2: "Included Thumbnail", 3: "Text First, Then Thumbnail" },
5729
5736
  provenance: "verified",
5737
+ available: isCameraCodec,
5730
5738
  description: "How much a detection push carries: text only, a thumbnail, or text then a thumbnail (CMD_INDOOR_PUSH_NOTIFY_TYPE 6020). All three values wire-verified live on a standalone T8171 over the cloud/WAN path \u2014 the 1700 control-payload wrapper, each one read back on the cloud param. A value outside the enum is REJECTED, not clamped: every neighbouring value is a real style, so a bad one would silently select the wrong notification.",
5731
5739
  write: (v, ctx) => {
5732
5740
  const style = coerceEnumValue(NotificationStyle2, v);
@@ -5745,6 +5753,7 @@ var CAMERA_MEMBERS = {
5745
5753
  kind: "enum",
5746
5754
  enumValues: { 0: "Off", 1: "Infrared", 2: "Full Color" },
5747
5755
  provenance: "verified",
5756
+ available: isCameraCodec,
5748
5757
  description: "Night-vision mode (NIGHT_VISION_TYPE 1277): 0 = off, 1 = infrared (B&W), 2 = full colour. \u2705 wire verified live (T8425 ch3): 1350 SET_PAYLOAD, mChannel 0, {channel:N, night_sion:mode}. Enum labels are best-guess. Some models omit full colour.",
5749
5758
  write: (v, ctx) => {
5750
5759
  const nv = coerceEnumValue(NightVision, v);
@@ -5770,6 +5779,7 @@ var CAMERA_MEMBERS = {
5770
5779
  kind: "enum",
5771
5780
  enumValues: STREAMING_QUALITY_TIERS,
5772
5781
  provenance: "verified",
5782
+ available: isCameraCodec,
5773
5783
  args: [{ name: "quality", kind: "enum", description: "A tier, or the name it maps to. 0 is Auto." }],
5774
5784
  ...accepts(),
5775
5785
  description: "Live-view quality as a tier, 0 = Auto (1020; the write rides 2730 APP_CMD_MULTI_CAM_SET_VIDEO_QUALITY). Distinct from recordingQuality, which is what gets stored. All four tiers wire-verified live on a T8170 and read back on 1020. Sent from this SDK to a standalone T8170 and T8171, tier 1 holds the live stream at 1280x720 and tier 0 restores the camera's own resolution ladder \u2014 see CAMERA_CMD.STREAMING_QUALITY_SET. A value outside the tier set is REJECTED, not clamped: every tier in range is a real quality, so a bad one would silently pick another.",
@@ -5787,6 +5797,7 @@ var CAMERA_MEMBERS = {
5787
5797
  param: CAMERA_CMD.RECORDING_QUALITY_SET,
5788
5798
  type: "string",
5789
5799
  provenance: "verified",
5800
+ available: isCameraCodec,
5790
5801
  decode: (raw) => decodeRecordingQualityTier(raw),
5791
5802
  decodedKind: "enum",
5792
5803
  decodedValues: Object.keys(RECORDING_QUALITY_TIERS).map(Number),
@@ -5811,6 +5822,7 @@ var CAMERA_MEMBERS = {
5811
5822
  type: "bool",
5812
5823
  kind: "boolean",
5813
5824
  provenance: "apk",
5825
+ available: isCameraCodec,
5814
5826
  description: "Anti-theft detection on/off (1015 APP_CMD_EAS_SWITCH; the app parses it as anti_theft_detection_switch). Adaptive scalar {value:0|1}. \u26A0\uFE0F Replay+readback confirmed on a HomeBase-attached T8425 only; standalone unverified and v3 devices use id 2735 \u2014 see CAMERA_CMD.EAS_SWITCH.",
5815
5827
  requires: [CAMERA_CMD.EAS_SWITCH],
5816
5828
  write: (v, ctx) => setScalar(CAMERA_CMD.EAS_SWITCH, asBool(v) ? 1 : 0, ctx, "auto")
@@ -5828,6 +5840,7 @@ var CAMERA_MEMBERS = {
5828
5840
  kind: "boolean",
5829
5841
  writeOnly: true,
5830
5842
  provenance: "verified",
5843
+ available: isCameraCodec,
5831
5844
  description: "Privacy mode (PRIVACY_MODE 6250) \u2014 the multi-frame burst the sink plays.",
5832
5845
  write: (v, ctx) => privacyCommand(asBool(v), ctx.channel)
5833
5846
  },
@@ -5841,6 +5854,7 @@ var CAMERA_MEMBERS = {
5841
5854
  type: "bool",
5842
5855
  kind: "boolean",
5843
5856
  provenance: "verified",
5857
+ available: isCameraCodec,
5844
5858
  readAvailable: (ctx) => !hasCapability(ctx, "doorbell"),
5845
5859
  readAliases: [{ paramType: CAMERA_CMD.DOORBELL_LED, available: (ctx) => hasCapability(ctx, "doorbell") }],
5846
5860
  requiresRead: true,
@@ -5864,28 +5878,28 @@ var CAMERA_MEMBERS = {
5864
5878
  * The refusal REJECTS on the four that answer with a promise, and THROWS on
5865
5879
  * {@link CAMERA_MEMBERS.recordFragments}, which answers with a handle.
5866
5880
  */
5867
- snapshotStored: provided("media", (m) => m.snapshotStored && (() => m.snapshotStored()), "Latest validated push thumbnail retained in memory.", ["snapshot"]),
5868
- snapshotLive: provided("media", (m, { ctx, read }) => async (opts) => {
5881
+ snapshotStored: provided("media", (m, { ctx }) => isCameraCodec(ctx) && m.snapshotStored && (() => m.snapshotStored()), "Latest validated push thumbnail retained in memory.", ["snapshot"]),
5882
+ snapshotLive: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && (async (opts) => {
5869
5883
  refuseWhenDisabled(ctx, read);
5870
5884
  return m.snapshotLive({ powered: poweredOf(ctx), ...opts });
5871
- }, "Fresh still decoded from a short live burst."),
5885
+ }), "Fresh still decoded from a short live burst."),
5872
5886
  live: provided("media", (m, { ctx, read }) => async (opts) => {
5873
5887
  refuseWhenDisabled(ctx, read);
5874
5888
  return m.live({ powered: poweredOf(ctx), ...opts });
5875
5889
  }, "Open a managed live stream."),
5876
- record: provided("media", (m, { ctx, read }) => async (...args) => {
5890
+ record: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && (async (...args) => {
5877
5891
  refuseWhenDisabled(ctx, read);
5878
5892
  return m.record(...args);
5879
- }, "Record N seconds \u2192 an mp4/h264 buffer."),
5880
- openReadable: provided("media", (m, { ctx, read }) => m.openReadable && (async (opts) => {
5893
+ }), "Record N seconds \u2192 an mp4/h264 buffer."),
5894
+ openReadable: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && m.openReadable && (async (opts) => {
5881
5895
  refuseWhenDisabled(ctx, read);
5882
5896
  return m.openReadable({ powered: poweredOf(ctx), ...opts });
5883
5897
  }), "Open a node:stream Readable of the live feed."),
5884
- recordFragments: provided("media", (m, { ctx, read }) => m.recordFragments && ((opts) => {
5898
+ recordFragments: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && m.recordFragments && ((opts) => {
5885
5899
  refuseWhenDisabled(ctx, read);
5886
5900
  return m.recordFragments({ powered: poweredOf(ctx), ...opts });
5887
5901
  }), "Continuous fragmented-MP4 (CMAF) recording."),
5888
- downloadRecording: provided("media", (m) => m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
5902
+ downloadRecording: provided("media", (m, { ctx }) => isCameraCodec(ctx) && m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
5889
5903
  /**
5890
5904
  * Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
5891
5905
  * not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
@@ -5893,7 +5907,7 @@ var CAMERA_MEMBERS = {
5893
5907
  * power hint `live` does; without it a battery camera talked to with no stream running would stream
5894
5908
  * unbounded.
5895
5909
  */
5896
- talkback: provided("media", (m, { ctx }) => m.talkback && ctx.paramIds.has(AUDIO_CMD.AUDIO_SPEAKER) && ((opts) => m.talkback({ powered: poweredOf(ctx), ...opts })), "Push audio from the host to the camera's speaker.")
5910
+ talkback: provided("media", (m, { ctx }) => isCameraCodec(ctx) && m.talkback && ctx.paramIds.has(AUDIO_CMD.AUDIO_SPEAKER) && ((opts) => m.talkback({ powered: poweredOf(ctx), ...opts })), "Push audio from the host to the camera's speaker.")
5897
5911
  };
5898
5912
  var CAMERA = {
5899
5913
  capability: "camera",
@@ -5904,6 +5918,8 @@ var CAMERA = {
5904
5918
  detection: { codecs: ["camera"] },
5905
5919
  /** Only the no-argument power verbs, which carry no value for a member to hold. */
5906
5920
  actions({ ctx, sink }) {
5921
+ if (!isCameraCodec(ctx))
5922
+ return {};
5907
5923
  return {
5908
5924
  on: () => sink.dispatch(powerCommand(true, ctx)),
5909
5925
  off: () => sink.dispatch(powerCommand(false, ctx))
@@ -11602,6 +11618,8 @@ var MODEL_REGISTRY = {
11602
11618
  T8442: { codec: "camera", caps: ["arming"], name: "Indoor/Outdoor Cam 1080p" },
11603
11619
  // Hardwired wall-light camera; mains power asserted in MAINS_CAMERA_MODELS.
11604
11620
  T84A1: { codec: "camera", name: "Wall Light Cam S100" },
11621
+ // Video lock: device_type says lock, it also streams (live view verified, HomeBase 3-attached).
11622
+ T8530: { codec: "lock", caps: ["camera"], name: "Video Smart Lock S330" },
11605
11623
  // Sensors — the concrete sensor kind is curated (DeviceType 2 alone is ambiguous).
11606
11624
  T8900: { codec: "sensor", caps: ["contact"], name: "Entry Sensor" },
11607
11625
  T8910: { codec: "sensor", caps: ["battery"], name: "Motion Sensor" },
@@ -18698,16 +18716,18 @@ function h265Geometry(sps) {
18698
18716
  };
18699
18717
  }
18700
18718
 
18701
- // dist/transport/p2p/live-stream.js
18702
- var CMD_VIDEO_FRAME2 = 1300;
18719
+ // dist/transport/p2p/audio-frame.js
18703
18720
  var CMD_AUDIO_FRAME2 = 1301;
18704
- var VIDEO_HEADER_LEN = 22;
18705
18721
  var AUDIO_HEADER_LEN = 16;
18706
18722
  var AUDIO_TYPE_OFFSET = 5;
18723
+ var AUDIO_CODECS = { 0: "aac-lc", 2: "g711a", 7: "aac-eld" };
18724
+
18725
+ // dist/transport/p2p/live-stream.js
18726
+ var CMD_VIDEO_FRAME2 = 1300;
18727
+ var VIDEO_HEADER_LEN = 22;
18707
18728
  var SC4 = Buffer.from([0, 0, 0, 1]);
18708
18729
  var SC3 = Buffer.from([0, 0, 1]);
18709
18730
  var AAC_ELD_AUDIO_SPECIFIC_CONFIG = Buffer.from("f8f03000", "hex");
18710
- var AUDIO_CODECS = { 0: "aac-lc", 2: "g711a", 7: "aac-eld" };
18711
18731
  var MAX_TRACED_DECODE_FAILURES = 3;
18712
18732
  var DEFAULT_KEEPALIVE_MS = 3e3;
18713
18733
  var LiveStream = class extends EventEmitter3 {
@@ -23423,6 +23443,8 @@ var RtcLiveStream = class extends EventEmitter7 {
23423
23443
  opts;
23424
23444
  running = false;
23425
23445
  startSegment = -1;
23446
+ onConnected = () => this.sendStart();
23447
+ onError = (error) => this.fail(error);
23426
23448
  keepalive;
23427
23449
  width = 0;
23428
23450
  height = 0;
@@ -23439,11 +23461,24 @@ var RtcLiveStream = class extends EventEmitter7 {
23439
23461
  start() {
23440
23462
  if (this.running)
23441
23463
  return this;
23442
- const { session, seg, accountId } = this.opts;
23464
+ const { session } = this.opts;
23443
23465
  this.running = true;
23466
+ this.startSegment = -1;
23444
23467
  session.on("mediaData", this.onMedia);
23445
23468
  session.on("commandData", this.onCommand);
23446
23469
  session.on("close", this.onClose);
23470
+ if (this.opts.nvr) {
23471
+ session.on("connected", this.onConnected);
23472
+ session.on("error", this.onError);
23473
+ session.connect().catch(this.onError);
23474
+ } else
23475
+ this.sendStart();
23476
+ return this;
23477
+ }
23478
+ sendStart() {
23479
+ if (!this.running || this.startSegment !== -1)
23480
+ return;
23481
+ const { session, seg, accountId, nvr } = this.opts;
23447
23482
  this.startSegment = seg.next();
23448
23483
  const sent2 = session.sendCommand(buildPortalPacket({
23449
23484
  commandId: PORTAL_CMD_SET_PAYLOAD,
@@ -23459,7 +23494,7 @@ var RtcLiveStream = class extends EventEmitter7 {
23459
23494
  camera_type: 0,
23460
23495
  key: "",
23461
23496
  msg_id: 116,
23462
- audio_chn: -1,
23497
+ audio_chn: nvr ? this.opts.channel : -1,
23463
23498
  streamtype: 2,
23464
23499
  stitch_mode: 1,
23465
23500
  chn_list: [{ index: 0, chn: this.opts.channel, sensor: 0, isUps: 0, isClicked: true }],
@@ -23471,7 +23506,7 @@ var RtcLiveStream = class extends EventEmitter7 {
23471
23506
  }));
23472
23507
  if (!sent2) {
23473
23508
  queueMicrotask(() => this.fail(new Error(`rtc live ${this.tag}: command channel not open`)));
23474
- return this;
23509
+ return;
23475
23510
  }
23476
23511
  this.keepalive = setInterval(() => {
23477
23512
  if (!session.sendRaw(PORTAL_KEEPALIVE))
@@ -23479,20 +23514,22 @@ var RtcLiveStream = class extends EventEmitter7 {
23479
23514
  }, PORTAL_KEEPALIVE_MS);
23480
23515
  this.keepalive.unref?.();
23481
23516
  this.opts.logger?.debug?.(`[rtc] ${this.tag} live start sent`);
23482
- return this;
23483
23517
  }
23484
23518
  stop() {
23485
23519
  if (!this.running)
23486
23520
  return;
23487
23521
  this.teardown();
23488
23522
  const { session, seg, accountId } = this.opts;
23489
- session.sendCommand(buildPortalPacket({
23490
- commandId: PORTAL_CMD_SET_PAYLOAD,
23491
- channel: PORTAL_STATION_CHANNEL,
23492
- segment: seg.next(),
23493
- isResponse: LIVE_STREAM_ID,
23494
- payload: { account_id: accountId, cmd: LIVE_STOP, payload: {} }
23495
- }));
23523
+ if (this.startSegment !== -1)
23524
+ session.sendCommand(buildPortalPacket({
23525
+ commandId: PORTAL_CMD_SET_PAYLOAD,
23526
+ channel: PORTAL_STATION_CHANNEL,
23527
+ segment: seg.next(),
23528
+ isResponse: LIVE_STREAM_ID,
23529
+ payload: { account_id: accountId, cmd: LIVE_STOP, payload: {} }
23530
+ }));
23531
+ if (this.opts.nvr)
23532
+ session.close();
23496
23533
  this.emit("stop");
23497
23534
  }
23498
23535
  teardown() {
@@ -23504,11 +23541,15 @@ var RtcLiveStream = class extends EventEmitter7 {
23504
23541
  session.off("mediaData", this.onMedia);
23505
23542
  session.off("commandData", this.onCommand);
23506
23543
  session.off("close", this.onClose);
23544
+ session.off("connected", this.onConnected);
23545
+ session.off("error", this.onError);
23507
23546
  }
23508
23547
  fail(err) {
23509
23548
  if (!this.running)
23510
23549
  return;
23511
23550
  this.teardown();
23551
+ if (this.opts.nvr)
23552
+ this.opts.session.close();
23512
23553
  this.emit("error", err);
23513
23554
  }
23514
23555
  /** The start's ACK: a non-zero result code ends the pull. */
@@ -23521,7 +23562,20 @@ var RtcLiveStream = class extends EventEmitter7 {
23521
23562
  }
23522
23563
  onMediaFrame(frame) {
23523
23564
  const h = parsePortalHeader(frame);
23524
- if (!h || h.commandId !== LIVE_MEDIA || h.channel !== LIVE_MEDIA_CHANNEL)
23565
+ if (!h || h.channel !== LIVE_MEDIA_CHANNEL)
23566
+ return;
23567
+ if (h.commandId === CMD_AUDIO_FRAME2 && this.opts.nvr) {
23568
+ if (h.paramLength !== frame.length - PORTAL_HEADER_LENGTH || h.paramLength <= AUDIO_HEADER_LEN)
23569
+ return;
23570
+ const body = frame.subarray(PORTAL_HEADER_LENGTH);
23571
+ const codec = AUDIO_CODECS[body[AUDIO_TYPE_OFFSET]];
23572
+ if (body.readUInt32LE(0) !== body.length - AUDIO_HEADER_LEN || codec !== "aac-lc" && codec !== "g711a")
23573
+ return;
23574
+ const audio = { codec, data: body.subarray(AUDIO_HEADER_LEN) };
23575
+ this.emit("audio", audio);
23576
+ return;
23577
+ }
23578
+ if (h.commandId !== LIVE_MEDIA)
23525
23579
  return;
23526
23580
  if (h.paramLength > frame.length - PORTAL_HEADER_LENGTH)
23527
23581
  return;
@@ -24782,38 +24836,54 @@ var RtcCommandRouter = class {
24782
24836
  * session. Recording has no wire here and is refused.
24783
24837
  */
24784
24838
  trackMediaProviderFor(route) {
24785
- const sn = route.stationSn;
24786
- const sourceFor = (hints = {}) => {
24787
- let source = this.callSources.get(sn);
24839
+ 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);
24840
+ }
24841
+ /** NVR media uses a dedicated call per camera, independent of station commands. */
24842
+ nvrMediaProviderFor(route) {
24843
+ return this.callMediaProviderFor(route, route.cameraSn, (session, accountId) => new RtcLiveStream({
24844
+ session,
24845
+ seg: new SegmentCounter(),
24846
+ stationSn: route.stationSn,
24847
+ channel: route.channel,
24848
+ accountId,
24849
+ logger: this.deps.logger,
24850
+ nvr: true
24851
+ }), "record is not available over an NVR portal call");
24852
+ }
24853
+ /** Share a dedicated-call source and its consumer operations under one route key. */
24854
+ callMediaProviderFor(route, key, makeStream, recordError, warmTimeoutMs) {
24855
+ const sourceFor = (opts = {}) => {
24856
+ opts.signal?.throwIfAborted();
24857
+ let source = this.callSources.get(key);
24788
24858
  if (!source) {
24789
24859
  source = new SharedLiveSource({
24790
24860
  makeStream: () => {
24791
24861
  const identity = this.deps.identity();
24792
24862
  if (!identity)
24793
- throw new Error(`rtc: not logged in, cannot call ${sn}`);
24794
- const session = this.newSession(route, route.adminUserId || identity.userId, identity);
24795
- return new RtcCallLiveStream(session, this.deps.logger);
24863
+ throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
24864
+ const accountId = route.adminUserId || identity.userId;
24865
+ return makeStream(this.newSession(route, accountId, identity), accountId);
24796
24866
  },
24797
- label: sn,
24867
+ label: key,
24798
24868
  logger: this.deps.logger,
24799
- powered: hints.powered,
24800
- preBufferSeconds: hints.preBufferSeconds,
24801
- warmTimeoutMs: 45e3
24869
+ powered: opts.powered,
24870
+ preBufferSeconds: opts.preBufferSeconds,
24871
+ warmTimeoutMs
24802
24872
  });
24803
- this.callSources.set(sn, source);
24873
+ this.callSources.set(key, source);
24804
24874
  }
24805
24875
  return source;
24806
24876
  };
24807
24877
  return {
24808
24878
  live: async (opts) => attachUnlessAborted(sourceFor(opts), opts?.signal),
24809
24879
  openReadable: async (opts) => openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts),
24810
- snapshotLive: (opts) => captureSnapshotFromShared(sourceFor(opts), {
24880
+ snapshotLive: async (opts) => captureSnapshotFromShared(sourceFor(opts), {
24811
24881
  ...opts,
24812
24882
  logger: this.deps.logger,
24813
24883
  ffmpegPath: this.deps.ffmpegPath
24814
24884
  }),
24815
24885
  record: async () => {
24816
- throw new Error(`record is not available for ${sn} over a portal call`);
24886
+ throw new Error(recordError);
24817
24887
  }
24818
24888
  };
24819
24889
  }
@@ -29446,19 +29516,24 @@ var EufyMega = class extends EventEmitter14 {
29446
29516
  }
29447
29517
  /**
29448
29518
  * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
29449
- * `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
29450
- * a T8N00, whose route carries commands only.
29519
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, unless an NVR camera has its own dedicated media call.
29451
29520
  */
29452
29521
  rtcMediaFor(sn) {
29453
29522
  const route = this.rtcRouteFor(sn);
29454
29523
  if (route?.mediaTrack)
29455
29524
  return this.rtc.trackMediaProviderFor(route);
29456
- if (!route?.attached || route.signalingMode === "call")
29525
+ if (!route?.attached)
29457
29526
  return void 0;
29458
- const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
29527
+ const camera = this.registry.list().find((d) => d.sn === sn);
29528
+ const raw = camera?.raw ?? {};
29459
29529
  const channel = typeof raw.device_channel === "number" ? raw.device_channel : void 0;
29460
29530
  if (channel === void 0 || this.registry.serialForFrame(route.stationSn, channel) !== sn)
29461
29531
  return void 0;
29532
+ if (route.signalingMode === "call") {
29533
+ if (!camera || !isNvrMediaCamera(camera))
29534
+ return void 0;
29535
+ return this.rtc.nvrMediaProviderFor({ ...route, cameraSn: sn, channel });
29536
+ }
29462
29537
  return this.rtc.mediaProviderFor({ ...route, cameraSn: sn, channel });
29463
29538
  }
29464
29539
  /**