@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.
- package/dist/client/eufy-mega.d.ts +1 -2
- package/dist/index.js +117 -42
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +22 -8
- package/dist/model/device-family.d.ts +2 -0
- package/dist/transport/p2p/audio-frame.d.ts +14 -0
- package/dist/transport/rtc/command-router.d.ts +4 -0
- package/dist/transport/rtc/live.d.ts +9 -4
- package/package.json +1 -1
|
@@ -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,
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
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.
|
|
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
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
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 ${
|
|
24794
|
-
const
|
|
24795
|
-
return
|
|
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:
|
|
24867
|
+
label: key,
|
|
24798
24868
|
logger: this.deps.logger,
|
|
24799
|
-
powered:
|
|
24800
|
-
preBufferSeconds:
|
|
24801
|
-
warmTimeoutMs
|
|
24869
|
+
powered: opts.powered,
|
|
24870
|
+
preBufferSeconds: opts.preBufferSeconds,
|
|
24871
|
+
warmTimeoutMs
|
|
24802
24872
|
});
|
|
24803
|
-
this.callSources.set(
|
|
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(
|
|
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,
|
|
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
|
|
29525
|
+
if (!route?.attached)
|
|
29457
29526
|
return void 0;
|
|
29458
|
-
const
|
|
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
|
/**
|