@apocaliss92/nodedreame 1.11.10 → 1.12.0
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/README.md +151 -5
- package/dist/index.cjs +2491 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1124 -6
- package/dist/index.d.ts +1124 -6
- package/dist/index.js +2446 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,17 +32,28 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AI_FEATURE_BIT: () => AI_FEATURE_BIT,
|
|
34
34
|
AI_FEATURE_JSON_KEY: () => AI_FEATURE_JSON_KEY,
|
|
35
|
+
ALIYUN_REGION_ID: () => ALIYUN_REGION_ID,
|
|
35
36
|
AUTO_SWITCH_JSON_KEY: () => AUTO_SWITCH_JSON_KEY,
|
|
36
37
|
BaseDevice: () => BaseDevice,
|
|
37
38
|
ChargingStatus: () => ChargingStatus,
|
|
38
39
|
CleaningMode: () => CleaningMode,
|
|
40
|
+
DRIVE_DIRECTIONS: () => DRIVE_DIRECTIONS,
|
|
39
41
|
DefaultCapabilityResolver: () => DefaultCapabilityResolver,
|
|
40
42
|
DreameApiError: () => DreameApiError,
|
|
41
43
|
DreameAuthError: () => DreameAuthError,
|
|
44
|
+
DreameCameraController: () => DreameCameraController,
|
|
45
|
+
DreameCameraStream: () => DreameCameraStream,
|
|
42
46
|
DreameDeviceOfflineError: () => DreameDeviceOfflineError,
|
|
43
47
|
DreameError: () => DreameError,
|
|
44
48
|
DreameTransportError: () => DreameTransportError,
|
|
49
|
+
DreameVideoSession: () => DreameVideoSession,
|
|
45
50
|
LIBRARY_NAME: () => LIBRARY_NAME,
|
|
51
|
+
LV_STATUS: () => LV_STATUS,
|
|
52
|
+
LvRtmpClient: () => LvRtmpClient,
|
|
53
|
+
MONITOR_AIID: () => MONITOR_AIID,
|
|
54
|
+
MONITOR_PIID: () => MONITOR_PIID,
|
|
55
|
+
MONITOR_SIID: () => MONITOR_SIID,
|
|
56
|
+
MONITOR_VENDOR_TOKEN: () => MONITOR_VENDOR_TOKEN,
|
|
46
57
|
MOWER_MODEL_CAPABILITIES: () => MODEL_CAPABILITIES2,
|
|
47
58
|
MOWER_TASK_SUBSTATES: () => MOWER_TASK_SUBSTATES,
|
|
48
59
|
MiotError: () => MiotError,
|
|
@@ -55,40 +66,72 @@ __export(index_exports, {
|
|
|
55
66
|
MowerStatus: () => MowerStatus,
|
|
56
67
|
MowerTaskStatus: () => MowerTaskStatus,
|
|
57
68
|
Nodreame: () => Nodreame,
|
|
69
|
+
PET_SOUNDS: () => PET_SOUNDS,
|
|
58
70
|
SuctionLevel: () => SuctionLevel,
|
|
71
|
+
TALK_AUDIO_HEADER_G711A: () => TALK_AUDIO_HEADER_G711A,
|
|
59
72
|
TaskStatus: () => TaskStatus,
|
|
73
|
+
VACUUM_ACTIONS: () => VACUUM_ACTIONS,
|
|
60
74
|
VACUUM_CONSUMABLES: () => VACUUM_CONSUMABLES,
|
|
61
75
|
VACUUM_MODEL_CAPABILITIES: () => MODEL_CAPABILITIES,
|
|
76
|
+
VACUUM_MOVE: () => VACUUM_MOVE,
|
|
77
|
+
VACUUM_SIID: () => VACUUM_SIID,
|
|
62
78
|
VacuumCapabilityResolver: () => VacuumCapabilityResolver,
|
|
63
79
|
VacuumDevice: () => VacuumDevice,
|
|
64
80
|
WaterVolume: () => WaterVolume,
|
|
81
|
+
aacSampleRate: () => aacSampleRate,
|
|
82
|
+
aacToAdts: () => aacToAdts,
|
|
83
|
+
avccToAnnexB: () => avccToAnnexB,
|
|
65
84
|
consumableSpec: () => consumableSpec,
|
|
66
85
|
createClientDumper: () => createClientDumper,
|
|
67
86
|
createDumper: () => createDumper,
|
|
87
|
+
createIotSession: () => createIotSession,
|
|
68
88
|
decodeAiFeature: () => decodeAiFeature,
|
|
69
89
|
decodeAutoSwitch: () => decodeAutoSwitch,
|
|
70
90
|
decodeAutoSwitchAll: () => decodeAutoSwitchAll,
|
|
91
|
+
decodeWifiSignalMap: () => decodeWifiSignalMap,
|
|
71
92
|
encodeAiFeatureWrite: () => encodeAiFeatureWrite,
|
|
72
93
|
encodeAutoSwitchWrite: () => encodeAutoSwitchWrite,
|
|
73
94
|
extractMowerConsumableValues: () => extractMowerConsumableValues,
|
|
95
|
+
getAliyunAuthCode: () => getAliyunAuthCode,
|
|
96
|
+
getDeviceVideoProfile: () => getDeviceVideoProfile,
|
|
74
97
|
getMowerCapabilities: () => getMowerCapabilities,
|
|
75
98
|
getVacuumCapabilities: () => getVacuumCapabilities,
|
|
99
|
+
getVideoAccessToken: () => getVideoAccessToken,
|
|
100
|
+
getVideoFamilyId: () => getVideoFamilyId,
|
|
101
|
+
hashAccessCode: () => hashAccessCode,
|
|
102
|
+
isAuthRefusedError: () => isAuthRefusedError,
|
|
76
103
|
isDreameConsumableKey: () => isDreameConsumableKey,
|
|
104
|
+
listDevices: () => listDevices,
|
|
105
|
+
loginByOauth: () => loginByOauth,
|
|
106
|
+
makeMonitorSession: () => makeMonitorSession,
|
|
77
107
|
mowerConsumableIndex: () => mowerConsumableIndex,
|
|
78
108
|
mowerFaultSeverity: () => mowerFaultSeverity,
|
|
109
|
+
parseAacConfig: () => parseAacConfig,
|
|
110
|
+
parseAvcConfig: () => parseAvcConfig,
|
|
111
|
+
parseConnectivity: () => parseConnectivity,
|
|
79
112
|
parseMowerConsumables: () => parseMowerConsumables,
|
|
80
113
|
parseMowerHeartbeat: () => parseMowerHeartbeat,
|
|
81
114
|
parseMowingProgress: () => parseMowingProgress,
|
|
115
|
+
parseObstacleData: () => parseObstacleData,
|
|
116
|
+
parsePersonFollow: () => parsePersonFollow,
|
|
117
|
+
parseRelayUrl: () => parseRelayUrl,
|
|
118
|
+
pcm16ToALaw: () => pcm16ToALaw,
|
|
119
|
+
pcm16leToALaw: () => pcm16leToALaw,
|
|
120
|
+
remoteDriveValue: () => remoteDriveValue,
|
|
82
121
|
renderMowerSvg: () => renderMowerSvg,
|
|
83
122
|
renderVacuumPng: () => renderVacuumPng,
|
|
123
|
+
renderWifiSignalPng: () => renderWifiSignalPng,
|
|
84
124
|
resolveCapabilities: () => resolveCapabilities,
|
|
85
|
-
|
|
125
|
+
signApiGatewayRequest: () => signApiGatewayRequest,
|
|
126
|
+
streamQuery: () => streamQuery,
|
|
127
|
+
supportedAutoSwitchKeys: () => supportedAutoSwitchKeys,
|
|
128
|
+
toVideoProfile: () => toVideoProfile
|
|
86
129
|
});
|
|
87
130
|
module.exports = __toCommonJS(index_exports);
|
|
88
131
|
|
|
89
132
|
// src/support/version.ts
|
|
90
133
|
var LIBRARY_NAME = "nodedreame";
|
|
91
|
-
var LIBRARY_VERSION = "1.
|
|
134
|
+
var LIBRARY_VERSION = "1.12.0";
|
|
92
135
|
|
|
93
136
|
// src/transport/errors.ts
|
|
94
137
|
var DreameError = class extends Error {
|
|
@@ -473,6 +516,34 @@ async function postForToken(ctx, body) {
|
|
|
473
516
|
return session;
|
|
474
517
|
}
|
|
475
518
|
|
|
519
|
+
// src/cloud/connectivity.ts
|
|
520
|
+
var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
|
|
521
|
+
var numOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
522
|
+
function parseBroker(bindDomain) {
|
|
523
|
+
const s = str(bindDomain);
|
|
524
|
+
if (!s) return null;
|
|
525
|
+
const idx = s.lastIndexOf(":");
|
|
526
|
+
if (idx < 0) return { host: s, port: null };
|
|
527
|
+
const port = Number(s.slice(idx + 1));
|
|
528
|
+
return { host: s.slice(0, idx), port: Number.isFinite(port) ? port : null };
|
|
529
|
+
}
|
|
530
|
+
function parseConnectivity(raw) {
|
|
531
|
+
const deviceInfo = raw["deviceInfo"] ?? {};
|
|
532
|
+
return {
|
|
533
|
+
online: raw["online"] === true,
|
|
534
|
+
connectionType: str(deviceInfo["scType"]),
|
|
535
|
+
mac: str(raw["mac"]),
|
|
536
|
+
broker: parseBroker(raw["bindDomain"]),
|
|
537
|
+
region: str(raw["region"]),
|
|
538
|
+
cloudVendor: str(raw["vendor"]),
|
|
539
|
+
firmwareVersion: str(raw["ver"]),
|
|
540
|
+
serialNumber: str(raw["sn"]),
|
|
541
|
+
subModel: str(raw["subModel"]),
|
|
542
|
+
battery: numOrNull(raw["battery"]),
|
|
543
|
+
statusCode: numOrNull(raw["latestStatus"])
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
476
547
|
// src/cloud/devices.ts
|
|
477
548
|
async function listDevices(input) {
|
|
478
549
|
const ctx = input.ctx ?? RequestContext.from({ ...input, host: input.apiHost });
|
|
@@ -517,7 +588,8 @@ function toDevice(rawIn) {
|
|
|
517
588
|
name,
|
|
518
589
|
online,
|
|
519
590
|
raw,
|
|
520
|
-
cloudState: parseCloudState(raw)
|
|
591
|
+
cloudState: parseCloudState(raw),
|
|
592
|
+
connectivity: parseConnectivity(raw)
|
|
521
593
|
};
|
|
522
594
|
if (rawIn.mac) {
|
|
523
595
|
device.mac = String(rawIn.mac);
|
|
@@ -717,6 +789,10 @@ var TypedEmitter = class {
|
|
|
717
789
|
emit(event, ...args) {
|
|
718
790
|
return this.#emitter.emit(event, ...args);
|
|
719
791
|
}
|
|
792
|
+
/** Number of listeners registered for `event`. */
|
|
793
|
+
listenerCount(event) {
|
|
794
|
+
return this.#emitter.listenerCount(event);
|
|
795
|
+
}
|
|
720
796
|
removeAllListeners() {
|
|
721
797
|
this.#emitter.removeAllListeners();
|
|
722
798
|
return this;
|
|
@@ -729,6 +805,24 @@ function defaultConnect(url, opts) {
|
|
|
729
805
|
const typed = client;
|
|
730
806
|
return typed;
|
|
731
807
|
}
|
|
808
|
+
function isAuthRefusedError(err) {
|
|
809
|
+
let cur = err;
|
|
810
|
+
for (let depth = 0; cur != null && depth < 6; depth += 1) {
|
|
811
|
+
if (typeof cur !== "object") {
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
const code = cur.code;
|
|
815
|
+
if (code === 4 || code === 5 || code === 134 || code === 135) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
const message = cur.message;
|
|
819
|
+
if (typeof message === "string" && /not authorized|bad user ?name|bad username or password|bad password/i.test(message)) {
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
cur = cur.cause;
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
732
826
|
var DreamePush = class extends TypedEmitter {
|
|
733
827
|
#device;
|
|
734
828
|
#session;
|
|
@@ -737,11 +831,15 @@ var DreamePush = class extends TypedEmitter {
|
|
|
737
831
|
#backoffMs;
|
|
738
832
|
#keepaliveSeconds;
|
|
739
833
|
#rejectUnauthorized;
|
|
834
|
+
#onAuthFailure;
|
|
835
|
+
#maxAuthRetries;
|
|
740
836
|
#topic;
|
|
741
837
|
#client = null;
|
|
742
838
|
#closed = false;
|
|
743
839
|
#tearingDown = false;
|
|
744
840
|
#reconnectTimer = null;
|
|
841
|
+
/** Consecutive auth-refused re-auths since the last successful connect. */
|
|
842
|
+
#authFailureCount = 0;
|
|
745
843
|
/**
|
|
746
844
|
* Per-instance bound close handler. Stored so we can pass the EXACT same
|
|
747
845
|
* function reference to both `client.on('close', …)` and
|
|
@@ -758,6 +856,8 @@ var DreamePush = class extends TypedEmitter {
|
|
|
758
856
|
this.#backoffMs = input.reconnectBackoffMs ?? 5e3;
|
|
759
857
|
this.#keepaliveSeconds = input.keepaliveSeconds ?? 60;
|
|
760
858
|
this.#rejectUnauthorized = input.rejectUnauthorized ?? false;
|
|
859
|
+
this.#onAuthFailure = input.onAuthFailure ?? null;
|
|
860
|
+
this.#maxAuthRetries = input.maxAuthRetries ?? 3;
|
|
761
861
|
this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
|
|
762
862
|
this.#onClose = () => {
|
|
763
863
|
if (this.#tearingDown) {
|
|
@@ -794,7 +894,11 @@ var DreamePush = class extends TypedEmitter {
|
|
|
794
894
|
this.#reconnectTimer = null;
|
|
795
895
|
}
|
|
796
896
|
await this.#teardownClient();
|
|
797
|
-
|
|
897
|
+
try {
|
|
898
|
+
await this.#connectAndSubscribe();
|
|
899
|
+
} catch {
|
|
900
|
+
this.#scheduleReconnect();
|
|
901
|
+
}
|
|
798
902
|
}
|
|
799
903
|
/** Tear down permanently. Closed subscriptions cannot be reopened. */
|
|
800
904
|
async close() {
|
|
@@ -831,6 +935,7 @@ var DreamePush = class extends TypedEmitter {
|
|
|
831
935
|
reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
|
|
832
936
|
return;
|
|
833
937
|
}
|
|
938
|
+
this.#authFailureCount = 0;
|
|
834
939
|
this.emit("connect");
|
|
835
940
|
resolve();
|
|
836
941
|
});
|
|
@@ -864,12 +969,55 @@ var DreamePush = class extends TypedEmitter {
|
|
|
864
969
|
if (this.#closed || this.#client) {
|
|
865
970
|
return;
|
|
866
971
|
}
|
|
867
|
-
void this.#
|
|
868
|
-
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
869
|
-
this.#scheduleReconnect();
|
|
870
|
-
});
|
|
972
|
+
void this.#attemptReconnect();
|
|
871
973
|
}, this.#backoffMs);
|
|
872
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* One reconnect attempt with reactive auth self-heal. If the broker refuses
|
|
977
|
+
* the CONNECT with an auth error (a stale access token) and an
|
|
978
|
+
* `onAuthFailure` provider is wired and the re-auth budget is not exhausted,
|
|
979
|
+
* mint a fresh session and retry with the new token instead of replaying the
|
|
980
|
+
* old one. Any other failure (or an exhausted budget) falls back to the plain
|
|
981
|
+
* backoff reconnect so bad credentials cannot hot-loop re-login.
|
|
982
|
+
*/
|
|
983
|
+
async #attemptReconnect() {
|
|
984
|
+
try {
|
|
985
|
+
await this.#connectAndSubscribe();
|
|
986
|
+
} catch (err) {
|
|
987
|
+
if (this.#closed) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (this.#onAuthFailure && isAuthRefusedError(err) && this.#authFailureCount < this.#maxAuthRetries) {
|
|
991
|
+
await this.#reauthenticateAndReconnect();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
995
|
+
this.#scheduleReconnect();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/** Mint a fresh session via `onAuthFailure`, adopt its token, and reconnect. */
|
|
999
|
+
async #reauthenticateAndReconnect() {
|
|
1000
|
+
this.#authFailureCount += 1;
|
|
1001
|
+
let session;
|
|
1002
|
+
try {
|
|
1003
|
+
session = await this.#onAuthFailure();
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
1006
|
+
if (!this.#client) {
|
|
1007
|
+
this.#scheduleReconnect();
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (this.#closed) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
this.#session = session;
|
|
1015
|
+
this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
|
|
1016
|
+
if (this.#client) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
void this.#attemptReconnect();
|
|
1020
|
+
}
|
|
873
1021
|
async #teardownClient() {
|
|
874
1022
|
const client = this.#client;
|
|
875
1023
|
this.#client = null;
|
|
@@ -1045,7 +1193,12 @@ function resolveCapabilities(model) {
|
|
|
1045
1193
|
// src/device/base-device.ts
|
|
1046
1194
|
function defaultBaseDeviceDeps() {
|
|
1047
1195
|
return {
|
|
1048
|
-
createPush: (device, session, region) => new DreamePush({
|
|
1196
|
+
createPush: (device, session, region, onAuthFailure) => new DreamePush({
|
|
1197
|
+
device,
|
|
1198
|
+
session,
|
|
1199
|
+
region,
|
|
1200
|
+
...onAuthFailure !== void 0 ? { onAuthFailure } : {}
|
|
1201
|
+
}),
|
|
1049
1202
|
getProperties: (base, props) => getProperties(base, props),
|
|
1050
1203
|
getCachedProperties: (base, props) => getCachedProperties(base, props),
|
|
1051
1204
|
setProperties: (base, writes) => setProperties(base, writes),
|
|
@@ -1057,6 +1210,7 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1057
1210
|
#device;
|
|
1058
1211
|
#region;
|
|
1059
1212
|
#sessionRef;
|
|
1213
|
+
#onAuthFailure;
|
|
1060
1214
|
#deps;
|
|
1061
1215
|
#fetchInitial;
|
|
1062
1216
|
#initialProps;
|
|
@@ -1072,6 +1226,7 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1072
1226
|
this.#device = input.device;
|
|
1073
1227
|
this.#region = input.region;
|
|
1074
1228
|
this.#sessionRef = input.sessionRef;
|
|
1229
|
+
this.#onAuthFailure = input.onAuthFailure;
|
|
1075
1230
|
this.#deps = input.deps ?? defaultBaseDeviceDeps();
|
|
1076
1231
|
this.#fetchInitial = input.fetchInitialValues ?? true;
|
|
1077
1232
|
this.#initialProps = input.initialProps ?? [];
|
|
@@ -1111,7 +1266,12 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1111
1266
|
}
|
|
1112
1267
|
/** Open the push, wire events, optionally seed the cache. */
|
|
1113
1268
|
async start() {
|
|
1114
|
-
const push = this.#deps.createPush(
|
|
1269
|
+
const push = this.#deps.createPush(
|
|
1270
|
+
this.#device,
|
|
1271
|
+
this.#sessionRef(),
|
|
1272
|
+
this.#region,
|
|
1273
|
+
this.#onAuthFailure
|
|
1274
|
+
);
|
|
1115
1275
|
this.#push = push;
|
|
1116
1276
|
push.on("properties", (changes) => this.#onProperties(changes));
|
|
1117
1277
|
push.on(
|
|
@@ -2014,8 +2174,1015 @@ var VacuumCapabilityResolver = class {
|
|
|
2014
2174
|
}
|
|
2015
2175
|
};
|
|
2016
2176
|
|
|
2177
|
+
// src/video/schemas.ts
|
|
2178
|
+
var import_zod3 = require("zod");
|
|
2179
|
+
var VideoAccessTokenResponseSchema = import_zod3.z.object({
|
|
2180
|
+
code: import_zod3.z.number().optional(),
|
|
2181
|
+
msg: import_zod3.z.string().nullish(),
|
|
2182
|
+
data: import_zod3.z.object({
|
|
2183
|
+
data: import_zod3.z.object({
|
|
2184
|
+
token: import_zod3.z.string(),
|
|
2185
|
+
userId: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional(),
|
|
2186
|
+
expireAt: import_zod3.z.number().optional()
|
|
2187
|
+
}).passthrough()
|
|
2188
|
+
}).passthrough()
|
|
2189
|
+
}).passthrough();
|
|
2190
|
+
var AuthCodeResponseSchema = import_zod3.z.object({
|
|
2191
|
+
code: import_zod3.z.number().optional(),
|
|
2192
|
+
msg: import_zod3.z.string().nullish(),
|
|
2193
|
+
data: import_zod3.z.string()
|
|
2194
|
+
}).passthrough();
|
|
2195
|
+
var FamilyIdResponseSchema = import_zod3.z.object({
|
|
2196
|
+
code: import_zod3.z.number().optional(),
|
|
2197
|
+
msg: import_zod3.z.string().nullish(),
|
|
2198
|
+
data: import_zod3.z.object({ data: import_zod3.z.object({ familyId: import_zod3.z.string() }).passthrough() }).passthrough()
|
|
2199
|
+
}).passthrough();
|
|
2200
|
+
var DeviceVideoInfoSchema = import_zod3.z.object({
|
|
2201
|
+
model: import_zod3.z.string().optional(),
|
|
2202
|
+
displayName: import_zod3.z.string().optional(),
|
|
2203
|
+
permit: import_zod3.z.string().optional(),
|
|
2204
|
+
videoDynamicVendor: import_zod3.z.boolean().optional(),
|
|
2205
|
+
defaultVendors: import_zod3.z.array(import_zod3.z.string()).optional(),
|
|
2206
|
+
productId: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional()
|
|
2207
|
+
}).passthrough();
|
|
2208
|
+
var DeviceInfoRecordSchema = import_zod3.z.object({
|
|
2209
|
+
did: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional(),
|
|
2210
|
+
model: import_zod3.z.string().optional(),
|
|
2211
|
+
customName: import_zod3.z.string().optional(),
|
|
2212
|
+
vendor: import_zod3.z.string().optional(),
|
|
2213
|
+
online: import_zod3.z.boolean().optional(),
|
|
2214
|
+
lwt: import_zod3.z.number().optional(),
|
|
2215
|
+
property: import_zod3.z.string().optional(),
|
|
2216
|
+
bindDomain: import_zod3.z.string().optional(),
|
|
2217
|
+
deviceInfo: DeviceVideoInfoSchema.optional()
|
|
2218
|
+
}).passthrough();
|
|
2219
|
+
var DeviceInfoResponseSchema = import_zod3.z.object({
|
|
2220
|
+
code: import_zod3.z.number().optional(),
|
|
2221
|
+
msg: import_zod3.z.string().nullish(),
|
|
2222
|
+
data: DeviceInfoRecordSchema
|
|
2223
|
+
}).passthrough();
|
|
2224
|
+
|
|
2225
|
+
// src/video/profile.ts
|
|
2226
|
+
var KNOWN_VENDORS = ["tx", "ali"];
|
|
2227
|
+
function asVendor(v) {
|
|
2228
|
+
return v === "tx" || v === "ali" ? v : null;
|
|
2229
|
+
}
|
|
2230
|
+
function parseProperty(property) {
|
|
2231
|
+
if (!property) {
|
|
2232
|
+
return { iotId: null, lwt: null };
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
const p = JSON.parse(property);
|
|
2236
|
+
return {
|
|
2237
|
+
iotId: typeof p.iotId === "string" && p.iotId.length > 0 ? p.iotId : null,
|
|
2238
|
+
lwt: typeof p.lwt === "number" ? p.lwt : null
|
|
2239
|
+
};
|
|
2240
|
+
} catch {
|
|
2241
|
+
return { iotId: null, lwt: null };
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function toVideoProfile(record) {
|
|
2245
|
+
const info = record.deviceInfo ?? {};
|
|
2246
|
+
const supportedVendors = (info.defaultVendors ?? []).filter(
|
|
2247
|
+
(v) => KNOWN_VENDORS.includes(v)
|
|
2248
|
+
);
|
|
2249
|
+
const prop = parseProperty(record.property);
|
|
2250
|
+
const online = record.online === true || record.lwt === 1 || prop.lwt === 1;
|
|
2251
|
+
return {
|
|
2252
|
+
did: String(record.did ?? ""),
|
|
2253
|
+
model: String(record.model ?? info.model ?? ""),
|
|
2254
|
+
displayName: info.displayName ?? null,
|
|
2255
|
+
videoCapable: info.permit === "video" || supportedVendors.length > 0,
|
|
2256
|
+
currentVendor: asVendor(record.vendor),
|
|
2257
|
+
supportedVendors,
|
|
2258
|
+
dynamicVendor: info.videoDynamicVendor === true,
|
|
2259
|
+
iotId: prop.iotId,
|
|
2260
|
+
online
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// src/video/client.ts
|
|
2265
|
+
var P_THIRD_VIDEO = "/dreame-third-video";
|
|
2266
|
+
var P_SMARTHOME = "/dreame-smarthome";
|
|
2267
|
+
var P_USER_IOT = "/dreame-user-iot";
|
|
2268
|
+
var DEFAULT_OS = "android";
|
|
2269
|
+
function resolveCtx(input) {
|
|
2270
|
+
return input.ctx ?? RequestContext.from({ ...input, host: input.apiHost });
|
|
2271
|
+
}
|
|
2272
|
+
function passthrough(input) {
|
|
2273
|
+
return {
|
|
2274
|
+
accessToken: input.session.accessToken,
|
|
2275
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
2276
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
async function getVideoAccessToken(input) {
|
|
2280
|
+
const ctx = resolveCtx(input);
|
|
2281
|
+
const raw = await httpPostJsonBody({
|
|
2282
|
+
ctx,
|
|
2283
|
+
path: `${P_THIRD_VIDEO}/tx/user/accesstoken`,
|
|
2284
|
+
body: { os: DEFAULT_OS },
|
|
2285
|
+
context: "video accesstoken",
|
|
2286
|
+
...passthrough(input)
|
|
2287
|
+
});
|
|
2288
|
+
const parsed = VideoAccessTokenResponseSchema.parse(raw);
|
|
2289
|
+
const d = parsed.data.data;
|
|
2290
|
+
return {
|
|
2291
|
+
token: d.token,
|
|
2292
|
+
userId: d.userId !== void 0 ? String(d.userId) : null,
|
|
2293
|
+
// Cloud reports expiry in epoch-seconds; expose epoch-ms like DreameSession.
|
|
2294
|
+
expiresAt: d.expireAt !== void 0 ? d.expireAt * 1e3 : 0
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
async function getAliyunAuthCode(input) {
|
|
2298
|
+
const ctx = resolveCtx(input);
|
|
2299
|
+
const raw = await httpPostJsonBody({
|
|
2300
|
+
ctx,
|
|
2301
|
+
path: `${P_SMARTHOME}/aliIot/getAuthCodeV3`,
|
|
2302
|
+
body: { os: DEFAULT_OS },
|
|
2303
|
+
context: "aliyun authCode",
|
|
2304
|
+
...passthrough(input)
|
|
2305
|
+
});
|
|
2306
|
+
return AuthCodeResponseSchema.parse(raw).data;
|
|
2307
|
+
}
|
|
2308
|
+
async function getVideoFamilyId(input) {
|
|
2309
|
+
const ctx = resolveCtx(input);
|
|
2310
|
+
const videoToken = input.videoToken ?? (await getVideoAccessToken({ ...input, ctx })).token;
|
|
2311
|
+
const raw = await httpPostJsonBody({
|
|
2312
|
+
ctx,
|
|
2313
|
+
path: `${P_THIRD_VIDEO}/tx/mgr/family/getFamilyId`,
|
|
2314
|
+
body: { accesstoken: videoToken, os: DEFAULT_OS },
|
|
2315
|
+
context: "video familyId",
|
|
2316
|
+
...passthrough(input)
|
|
2317
|
+
});
|
|
2318
|
+
return FamilyIdResponseSchema.parse(raw).data.data.familyId;
|
|
2319
|
+
}
|
|
2320
|
+
async function getDeviceVideoProfile(input) {
|
|
2321
|
+
const ctx = resolveCtx(input);
|
|
2322
|
+
const raw = await httpPostJsonBody({
|
|
2323
|
+
ctx,
|
|
2324
|
+
path: `${P_USER_IOT}/iotuserbind/device/info`,
|
|
2325
|
+
body: { did: input.did },
|
|
2326
|
+
context: "device video info",
|
|
2327
|
+
...passthrough(input)
|
|
2328
|
+
});
|
|
2329
|
+
return toVideoProfile(DeviceInfoResponseSchema.parse(raw).data);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/video/aliyun/constants.ts
|
|
2333
|
+
var ALIYUN_APP_KEY = "33974273";
|
|
2334
|
+
var ALIYUN_APP_SECRET = "65a62f03ae5eda97af962fecc2e3efa6";
|
|
2335
|
+
var ALIYUN_REGION_ID = {
|
|
2336
|
+
eu: "eu-central-1",
|
|
2337
|
+
de: "eu-central-1",
|
|
2338
|
+
us: "us-east-1",
|
|
2339
|
+
cn: "cn-shanghai",
|
|
2340
|
+
ru: "eu-central-1",
|
|
2341
|
+
sg: "ap-southeast-1",
|
|
2342
|
+
in: "ap-southeast-1",
|
|
2343
|
+
tw: "cn-shanghai"
|
|
2344
|
+
};
|
|
2345
|
+
function livingAccountHost(regionId) {
|
|
2346
|
+
return `living-account.${regionId}.aliyuncs.com`;
|
|
2347
|
+
}
|
|
2348
|
+
function apiIotHost(regionId) {
|
|
2349
|
+
return `${regionId}.api-iot.aliyuncs.com`;
|
|
2350
|
+
}
|
|
2351
|
+
var VISION_STREAM_QUERY = "/vision/customer/stream/query";
|
|
2352
|
+
|
|
2353
|
+
// src/video/aliyun/identity.ts
|
|
2354
|
+
var import_node_crypto3 = require("crypto");
|
|
2355
|
+
var import_zod4 = require("zod");
|
|
2356
|
+
|
|
2357
|
+
// src/video/aliyun/signing.ts
|
|
2358
|
+
var import_node_crypto2 = require("crypto");
|
|
2359
|
+
var CT_JSON = "application/json; charset=UTF-8";
|
|
2360
|
+
var CT_FORM = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
2361
|
+
function md5Base64(s) {
|
|
2362
|
+
return (0, import_node_crypto2.createHash)("md5").update(s, "utf8").digest("base64");
|
|
2363
|
+
}
|
|
2364
|
+
function signApiGatewayRequest(input) {
|
|
2365
|
+
const accept = CT_JSON;
|
|
2366
|
+
const contentType = input.json !== void 0 ? CT_JSON : CT_FORM;
|
|
2367
|
+
const date = input.date ?? (/* @__PURE__ */ new Date()).toUTCString();
|
|
2368
|
+
let body;
|
|
2369
|
+
let contentMd5 = "";
|
|
2370
|
+
let formParams;
|
|
2371
|
+
if (input.json !== void 0) {
|
|
2372
|
+
body = JSON.stringify(input.json);
|
|
2373
|
+
contentMd5 = md5Base64(body);
|
|
2374
|
+
} else {
|
|
2375
|
+
const form = input.form ?? {};
|
|
2376
|
+
body = Object.entries(form).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2377
|
+
formParams = form;
|
|
2378
|
+
}
|
|
2379
|
+
const caHeaders = {
|
|
2380
|
+
"X-Ca-Key": input.appKey,
|
|
2381
|
+
"X-Ca-Nonce": input.nonce ?? crypto.randomUUID().toUpperCase(),
|
|
2382
|
+
"X-Ca-Timestamp": String(input.timestamp ?? Date.now()),
|
|
2383
|
+
"X-Ca-Version": "1",
|
|
2384
|
+
...input.extraCaHeaders ?? {}
|
|
2385
|
+
};
|
|
2386
|
+
const signedHeaderNames = Object.keys(caHeaders).sort(
|
|
2387
|
+
(a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0
|
|
2388
|
+
);
|
|
2389
|
+
const headerBlock = signedHeaderNames.map((k) => `${k}:${caHeaders[k]}
|
|
2390
|
+
`).join("");
|
|
2391
|
+
let resource = input.path;
|
|
2392
|
+
if (formParams && Object.keys(formParams).length > 0) {
|
|
2393
|
+
const sorted = Object.keys(formParams).sort();
|
|
2394
|
+
resource += "?" + sorted.map((k) => `${k}=${formParams[k]}`).join("&");
|
|
2395
|
+
}
|
|
2396
|
+
const stringToSign = `${input.method}
|
|
2397
|
+
${accept}
|
|
2398
|
+
${contentMd5}
|
|
2399
|
+
${contentType}
|
|
2400
|
+
${date}
|
|
2401
|
+
${headerBlock}${resource}`;
|
|
2402
|
+
const signature = (0, import_node_crypto2.createHmac)("sha1", input.appSecret).update(stringToSign, "utf8").digest("base64");
|
|
2403
|
+
const headers = {
|
|
2404
|
+
"Content-Type": contentType,
|
|
2405
|
+
Accept: accept,
|
|
2406
|
+
Date: date,
|
|
2407
|
+
"X-Ca-Signature": signature,
|
|
2408
|
+
"X-Ca-Signature-Method": "HmacSHA1",
|
|
2409
|
+
"X-Ca-Signature-Headers": signedHeaderNames.join(","),
|
|
2410
|
+
...caHeaders
|
|
2411
|
+
};
|
|
2412
|
+
if (contentMd5) {
|
|
2413
|
+
headers["Content-MD5"] = contentMd5;
|
|
2414
|
+
}
|
|
2415
|
+
return { headers, body };
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// src/video/aliyun/identity.ts
|
|
2419
|
+
var LoginByOauthSchema = import_zod4.z.object({
|
|
2420
|
+
data: import_zod4.z.object({
|
|
2421
|
+
data: import_zod4.z.object({
|
|
2422
|
+
loginSuccessResult: import_zod4.z.object({
|
|
2423
|
+
sid: import_zod4.z.string(),
|
|
2424
|
+
refreshToken: import_zod4.z.string().optional(),
|
|
2425
|
+
token: import_zod4.z.string().optional(),
|
|
2426
|
+
openAccount: import_zod4.z.object({ openId: import_zod4.z.string().optional() }).passthrough().optional()
|
|
2427
|
+
}).passthrough()
|
|
2428
|
+
}).passthrough()
|
|
2429
|
+
}).passthrough()
|
|
2430
|
+
}).passthrough();
|
|
2431
|
+
var CreateSessionSchema = import_zod4.z.object({
|
|
2432
|
+
code: import_zod4.z.number().optional(),
|
|
2433
|
+
message: import_zod4.z.string().nullish(),
|
|
2434
|
+
data: import_zod4.z.object({
|
|
2435
|
+
iotToken: import_zod4.z.string(),
|
|
2436
|
+
refreshToken: import_zod4.z.string().optional(),
|
|
2437
|
+
identityId: import_zod4.z.string().optional(),
|
|
2438
|
+
iotTokenExpire: import_zod4.z.number().optional()
|
|
2439
|
+
}).passthrough()
|
|
2440
|
+
}).passthrough();
|
|
2441
|
+
async function postJson(url, headers, body, fetchImpl, ctx) {
|
|
2442
|
+
const res = await fetchImpl(url, { method: "POST", headers, body });
|
|
2443
|
+
const text = await res.text();
|
|
2444
|
+
try {
|
|
2445
|
+
return JSON.parse(text);
|
|
2446
|
+
} catch {
|
|
2447
|
+
throw new DreameApiError(`${ctx} returned non-JSON (status ${res.status})`, res.status);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
async function loginByOauth(input) {
|
|
2451
|
+
const host = livingAccountHost(input.regionId);
|
|
2452
|
+
const riskControlInfo = {
|
|
2453
|
+
createIfNotExist: "true",
|
|
2454
|
+
sdkVersion: "3.4.0.27",
|
|
2455
|
+
openaccountVersion: "3.4.0.27",
|
|
2456
|
+
platformName: "iOS",
|
|
2457
|
+
netType: "wifi",
|
|
2458
|
+
brand: "Apple",
|
|
2459
|
+
USE_H5_NC: "true",
|
|
2460
|
+
deviceId: input.deviceId ?? (0, import_node_crypto3.randomUUID)(),
|
|
2461
|
+
locale: "en-US",
|
|
2462
|
+
platformVersion: "26.5.2",
|
|
2463
|
+
platform: "iPhone",
|
|
2464
|
+
appKey: ALIYUN_APP_KEY,
|
|
2465
|
+
USE_OA_PWD_ENCRYPT: "true",
|
|
2466
|
+
model: "iPhone16,2",
|
|
2467
|
+
utdid: input.utdid ?? (0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 20),
|
|
2468
|
+
appVersion: "1895"
|
|
2469
|
+
};
|
|
2470
|
+
const value = JSON.stringify({
|
|
2471
|
+
riskControlInfo,
|
|
2472
|
+
oauthPlateform: 23,
|
|
2473
|
+
oauthAppKey: ALIYUN_APP_KEY,
|
|
2474
|
+
authCode: input.authCode
|
|
2475
|
+
});
|
|
2476
|
+
const signed = signApiGatewayRequest({
|
|
2477
|
+
method: "POST",
|
|
2478
|
+
host,
|
|
2479
|
+
path: "/api/prd/loginbyoauth.json",
|
|
2480
|
+
appKey: ALIYUN_APP_KEY,
|
|
2481
|
+
appSecret: ALIYUN_APP_SECRET,
|
|
2482
|
+
form: { loginByOauthRequest: value }
|
|
2483
|
+
});
|
|
2484
|
+
const json = await postJson(
|
|
2485
|
+
`https://${host}/api/prd/loginbyoauth.json`,
|
|
2486
|
+
signed.headers,
|
|
2487
|
+
signed.body,
|
|
2488
|
+
input.fetchImpl ?? defaultFetch,
|
|
2489
|
+
"loginbyoauth"
|
|
2490
|
+
);
|
|
2491
|
+
const r = LoginByOauthSchema.parse(json).data.data.loginSuccessResult;
|
|
2492
|
+
return {
|
|
2493
|
+
sid: r.sid,
|
|
2494
|
+
refreshToken: r.refreshToken ?? "",
|
|
2495
|
+
openId: r.openAccount?.openId ?? ""
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
async function createIotSession(input) {
|
|
2499
|
+
const host = apiIotHost(input.regionId);
|
|
2500
|
+
const path = "/account/createSessionByAuthCode";
|
|
2501
|
+
const payload = {
|
|
2502
|
+
id: (0, import_node_crypto3.randomUUID)().toUpperCase(),
|
|
2503
|
+
params: { request: { appKey: ALIYUN_APP_KEY, authCode: input.sid, accountType: "OA_SESSION" } },
|
|
2504
|
+
request: { apiVer: "1.0.4", language: "en-US", appKey: ALIYUN_APP_KEY },
|
|
2505
|
+
version: "1.0.0"
|
|
2506
|
+
};
|
|
2507
|
+
const signed = signApiGatewayRequest({
|
|
2508
|
+
method: "POST",
|
|
2509
|
+
host,
|
|
2510
|
+
path,
|
|
2511
|
+
appKey: ALIYUN_APP_KEY,
|
|
2512
|
+
appSecret: ALIYUN_APP_SECRET,
|
|
2513
|
+
json: payload,
|
|
2514
|
+
extraCaHeaders: { "X-Ca-Stage": "RELEASE" }
|
|
2515
|
+
});
|
|
2516
|
+
const json = await postJson(
|
|
2517
|
+
`https://${host}${path}`,
|
|
2518
|
+
signed.headers,
|
|
2519
|
+
signed.body,
|
|
2520
|
+
input.fetchImpl ?? defaultFetch,
|
|
2521
|
+
"createSessionByAuthCode"
|
|
2522
|
+
);
|
|
2523
|
+
const parsed = CreateSessionSchema.parse(json);
|
|
2524
|
+
if (parsed.code !== 200 && parsed.code !== 0) {
|
|
2525
|
+
throw new DreameApiError(`createSession rejected: code=${parsed.code} msg=${parsed.message ?? "?"}`, 200, parsed);
|
|
2526
|
+
}
|
|
2527
|
+
const d = parsed.data;
|
|
2528
|
+
return {
|
|
2529
|
+
iotToken: d.iotToken,
|
|
2530
|
+
refreshToken: d.refreshToken ?? "",
|
|
2531
|
+
identityId: d.identityId ?? "",
|
|
2532
|
+
expiresAt: Date.now() + (d.iotTokenExpire ?? 72e3) * 1e3
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// src/video/aliyun/vision.ts
|
|
2537
|
+
var import_zod5 = require("zod");
|
|
2538
|
+
var VISION_DEVICE_OFFLINE = 9201;
|
|
2539
|
+
var StreamQueryResponseSchema = import_zod5.z.object({
|
|
2540
|
+
code: import_zod5.z.number().optional(),
|
|
2541
|
+
message: import_zod5.z.string().nullish(),
|
|
2542
|
+
data: import_zod5.z.object({
|
|
2543
|
+
typeList: import_zod5.z.array(import_zod5.z.string()).optional(),
|
|
2544
|
+
relayUrl: import_zod5.z.string().optional(),
|
|
2545
|
+
relayDecryptKey: import_zod5.z.object({ iv: import_zod5.z.string().optional(), key: import_zod5.z.string().optional() }).passthrough().optional(),
|
|
2546
|
+
p2pInfo: import_zod5.z.object({
|
|
2547
|
+
stunUrl: import_zod5.z.string().optional(),
|
|
2548
|
+
alterStunUrl: import_zod5.z.string().optional(),
|
|
2549
|
+
signalUrl: import_zod5.z.string().optional(),
|
|
2550
|
+
portDetectStunUrlList: import_zod5.z.array(import_zod5.z.string()).optional()
|
|
2551
|
+
}).passthrough().optional(),
|
|
2552
|
+
supportVisionPtz: import_zod5.z.boolean().optional()
|
|
2553
|
+
}).passthrough().optional()
|
|
2554
|
+
}).passthrough();
|
|
2555
|
+
async function streamQuery(input) {
|
|
2556
|
+
const appKey = input.appKey ?? ALIYUN_APP_KEY;
|
|
2557
|
+
const appSecret = input.appSecret ?? ALIYUN_APP_SECRET;
|
|
2558
|
+
const host = apiIotHost(input.regionId);
|
|
2559
|
+
const payload = {
|
|
2560
|
+
id: crypto.randomUUID().toUpperCase(),
|
|
2561
|
+
params: {
|
|
2562
|
+
streamType: input.streamType ?? 0,
|
|
2563
|
+
needDomainName: true,
|
|
2564
|
+
relayEncryptType: 0,
|
|
2565
|
+
clientType: "iOS",
|
|
2566
|
+
iotId: input.iotId,
|
|
2567
|
+
enablePortPredict: true,
|
|
2568
|
+
enableWebSocket: true,
|
|
2569
|
+
forceIFrame: true,
|
|
2570
|
+
cacheDuration: 3e3,
|
|
2571
|
+
relayEncrypted: input.relayEncrypted ?? false
|
|
2572
|
+
},
|
|
2573
|
+
request: { language: "en-US", appKey, iotToken: input.iotToken, apiVer: "2.2.0" },
|
|
2574
|
+
version: "1.0.0"
|
|
2575
|
+
};
|
|
2576
|
+
const signed = signApiGatewayRequest({
|
|
2577
|
+
method: "POST",
|
|
2578
|
+
host,
|
|
2579
|
+
path: VISION_STREAM_QUERY,
|
|
2580
|
+
appKey,
|
|
2581
|
+
appSecret,
|
|
2582
|
+
json: payload,
|
|
2583
|
+
extraCaHeaders: { "X-Ca-Stage": "RELEASE" }
|
|
2584
|
+
});
|
|
2585
|
+
const fetchImpl = input.fetchImpl ?? defaultFetch;
|
|
2586
|
+
const res = await fetchImpl(`https://${host}${VISION_STREAM_QUERY}`, {
|
|
2587
|
+
method: "POST",
|
|
2588
|
+
headers: signed.headers,
|
|
2589
|
+
body: signed.body,
|
|
2590
|
+
...input.signal ? { signal: input.signal } : {}
|
|
2591
|
+
});
|
|
2592
|
+
const text = await res.text();
|
|
2593
|
+
let json;
|
|
2594
|
+
try {
|
|
2595
|
+
json = JSON.parse(text);
|
|
2596
|
+
} catch {
|
|
2597
|
+
throw new DreameApiError(`stream/query returned non-JSON (status ${res.status})`, res.status);
|
|
2598
|
+
}
|
|
2599
|
+
const parsed = StreamQueryResponseSchema.parse(json);
|
|
2600
|
+
if (parsed.code === VISION_DEVICE_OFFLINE) {
|
|
2601
|
+
throw new DreameDeviceOfflineError(
|
|
2602
|
+
`stream/query: device video agent offline (code ${VISION_DEVICE_OFFLINE}) \u2014 retry to wake`,
|
|
2603
|
+
res.status,
|
|
2604
|
+
parsed
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
if (parsed.code !== 200 && parsed.code !== 0) {
|
|
2608
|
+
throw new DreameApiError(
|
|
2609
|
+
`stream/query rejected: code=${parsed.code} msg=${parsed.message ?? "?"}`,
|
|
2610
|
+
res.status,
|
|
2611
|
+
parsed
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
const d = parsed.data ?? {};
|
|
2615
|
+
const dk = d.relayDecryptKey;
|
|
2616
|
+
const hasKey = dk && dk.iv && dk.key;
|
|
2617
|
+
const p = d.p2pInfo;
|
|
2618
|
+
const p2pInfo = p ? {
|
|
2619
|
+
...p.stunUrl !== void 0 ? { stunUrl: p.stunUrl } : {},
|
|
2620
|
+
...p.alterStunUrl !== void 0 ? { alterStunUrl: p.alterStunUrl } : {},
|
|
2621
|
+
...p.signalUrl !== void 0 ? { signalUrl: p.signalUrl } : {},
|
|
2622
|
+
...p.portDetectStunUrlList !== void 0 ? { portDetectStunUrlList: p.portDetectStunUrlList } : {}
|
|
2623
|
+
} : null;
|
|
2624
|
+
return {
|
|
2625
|
+
typeList: d.typeList ?? [],
|
|
2626
|
+
relayUrl: d.relayUrl ?? null,
|
|
2627
|
+
relayDecryptKey: hasKey ? { iv: dk.iv, key: dk.key } : null,
|
|
2628
|
+
p2pInfo,
|
|
2629
|
+
...d.supportVisionPtz !== void 0 ? { supportVisionPtz: d.supportVisionPtz } : {}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/video/aliyun/session.ts
|
|
2634
|
+
var DEFAULT_WAKE_TIMEOUT_MS = 3e4;
|
|
2635
|
+
var DEFAULT_WAKE_POLL_MS = 2e3;
|
|
2636
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2637
|
+
var REFRESH_LEEWAY_MS = 6e4;
|
|
2638
|
+
var DreameVideoSession = class {
|
|
2639
|
+
#session;
|
|
2640
|
+
#region;
|
|
2641
|
+
#regionId;
|
|
2642
|
+
#fetchImpl;
|
|
2643
|
+
#iot = null;
|
|
2644
|
+
#inFlight = null;
|
|
2645
|
+
constructor(input) {
|
|
2646
|
+
this.#session = input.session;
|
|
2647
|
+
this.#region = input.region;
|
|
2648
|
+
this.#regionId = ALIYUN_REGION_ID[input.region];
|
|
2649
|
+
this.#fetchImpl = input.fetchImpl;
|
|
2650
|
+
}
|
|
2651
|
+
/** The resolved Aliyun IoT region id (e.g. `eu-central-1`). */
|
|
2652
|
+
get regionId() {
|
|
2653
|
+
return this.#regionId;
|
|
2654
|
+
}
|
|
2655
|
+
/** Mint (or reuse) a valid Aliyun IoT session, refreshing before expiry. */
|
|
2656
|
+
async ensureIotSession() {
|
|
2657
|
+
const cur = this.#iot;
|
|
2658
|
+
if (cur && Date.now() < cur.expiresAt - REFRESH_LEEWAY_MS) {
|
|
2659
|
+
return cur;
|
|
2660
|
+
}
|
|
2661
|
+
if (this.#inFlight) {
|
|
2662
|
+
return this.#inFlight;
|
|
2663
|
+
}
|
|
2664
|
+
this.#inFlight = this.#mint();
|
|
2665
|
+
try {
|
|
2666
|
+
this.#iot = await this.#inFlight;
|
|
2667
|
+
return this.#iot;
|
|
2668
|
+
} finally {
|
|
2669
|
+
this.#inFlight = null;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
async #mint() {
|
|
2673
|
+
const base = {
|
|
2674
|
+
session: this.#session,
|
|
2675
|
+
region: this.#region,
|
|
2676
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
2677
|
+
};
|
|
2678
|
+
const authCode = await getAliyunAuthCode(base);
|
|
2679
|
+
const oa = await loginByOauth({
|
|
2680
|
+
authCode,
|
|
2681
|
+
regionId: this.#regionId,
|
|
2682
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
2683
|
+
});
|
|
2684
|
+
return createIotSession({
|
|
2685
|
+
sid: oa.sid,
|
|
2686
|
+
regionId: this.#regionId,
|
|
2687
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Mint a fresh LinkVisual stream descriptor for a device. `encrypted:false`
|
|
2692
|
+
* (default) yields a plain RTMP relay URL directly playable by ffmpeg.
|
|
2693
|
+
*
|
|
2694
|
+
* A camera that has been idle reports "device offline" until the query wakes
|
|
2695
|
+
* its video agent via the LinkVisual cloud. This transparently retries on that
|
|
2696
|
+
* (up to `wakeTimeoutMs`, default 30s) so a single call returns a live stream;
|
|
2697
|
+
* pass `wakeTimeoutMs: 0` to fail fast instead.
|
|
2698
|
+
*/
|
|
2699
|
+
async getStreamInfo(iotId, opts) {
|
|
2700
|
+
const deadline = Date.now() + (opts?.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS);
|
|
2701
|
+
const pollMs = opts?.pollMs ?? DEFAULT_WAKE_POLL_MS;
|
|
2702
|
+
for (; ; ) {
|
|
2703
|
+
const iot = await this.ensureIotSession();
|
|
2704
|
+
try {
|
|
2705
|
+
return await streamQuery({
|
|
2706
|
+
regionId: this.#regionId,
|
|
2707
|
+
iotToken: iot.iotToken,
|
|
2708
|
+
iotId,
|
|
2709
|
+
relayEncrypted: opts?.encrypted ?? false,
|
|
2710
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
2711
|
+
});
|
|
2712
|
+
} catch (err) {
|
|
2713
|
+
if (err instanceof DreameDeviceOfflineError && Date.now() + pollMs < deadline) {
|
|
2714
|
+
await sleep(pollMs);
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
throw err;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Convenience: mint a fresh plain RTMP relay URL for a device, or `null` when
|
|
2723
|
+
* the device is not currently streaming (no relay available).
|
|
2724
|
+
*/
|
|
2725
|
+
async getRtmpUrl(iotId) {
|
|
2726
|
+
const info = await this.getStreamInfo(iotId, { encrypted: false });
|
|
2727
|
+
return info.relayUrl;
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
// src/video/monitor/constants.ts
|
|
2732
|
+
var MONITOR_SIID = 10001;
|
|
2733
|
+
var MONITOR_AIID = {
|
|
2734
|
+
/** Stream lifecycle + keep-alive (monitor start/end, keep_alive). */
|
|
2735
|
+
CAMERA_OPERATE: 1,
|
|
2736
|
+
/** Two-way intercom start/end. */
|
|
2737
|
+
VOICE_OPERATE: 2,
|
|
2738
|
+
/** Generic property-scoped operations. */
|
|
2739
|
+
PROPERTY_OPERATE: 3,
|
|
2740
|
+
/** Privacy access-code gate (launch/verify/set/reset). */
|
|
2741
|
+
ACCESS_CODE_OPERATE: 4,
|
|
2742
|
+
/** Video-vendor SDK init / switch. */
|
|
2743
|
+
VIDEO_VENDOR: 7
|
|
2744
|
+
};
|
|
2745
|
+
var MONITOR_PIID = {
|
|
2746
|
+
MONITOR_STATUS: 1,
|
|
2747
|
+
MONITOR_AUDIO_STATUS: 2,
|
|
2748
|
+
MONITOR_RECORD_STATUS: 4,
|
|
2749
|
+
TAKE_PHOTO: 5,
|
|
2750
|
+
KEEP_ALIVE: 6,
|
|
2751
|
+
MONITOR_FAULTS: 7,
|
|
2752
|
+
FILL_LIGHT_SET: 9,
|
|
2753
|
+
CAMERA_LIGHT_SWITCH: 10,
|
|
2754
|
+
VIDEO_VENDOR_STATUS: 11,
|
|
2755
|
+
GET_PROPERTY: 99,
|
|
2756
|
+
PERSON_DATA: 110,
|
|
2757
|
+
VIDEO_OBSTACLE_SWITCH: 111,
|
|
2758
|
+
VIDEO_OBSTACLE_DATA: 112,
|
|
2759
|
+
MONITOR_TASK_STATUS: 103,
|
|
2760
|
+
UPLOAD_STATUS: 1003,
|
|
2761
|
+
GET_ACCESS_CODE: 1100,
|
|
2762
|
+
SET_ACCESS_CODE: 1101,
|
|
2763
|
+
VERIFY_ACCESS_CODE: 1102,
|
|
2764
|
+
MONITOR_REST_MODE: 1103,
|
|
2765
|
+
SET_ACCESS_HOT_STATUS: 1104,
|
|
2766
|
+
MONITOR_SPACE: 2003
|
|
2767
|
+
};
|
|
2768
|
+
var MONITOR_VENDOR_TOKEN = {
|
|
2769
|
+
ali: "alify",
|
|
2770
|
+
tx: "tx"
|
|
2771
|
+
};
|
|
2772
|
+
var VACUUM_SIID = 4;
|
|
2773
|
+
var VACUUM_MOVE = {
|
|
2774
|
+
/** Remote-drive state property (set_properties), sent ~1 Hz while held. */
|
|
2775
|
+
REMOTE_STATE_PIID: 15,
|
|
2776
|
+
/** Work-mode action instance (go-to-point, cruise, person-follow start). */
|
|
2777
|
+
WORK_AIID: 1,
|
|
2778
|
+
/** Work-mode stop action instance (person-follow stop). */
|
|
2779
|
+
STOP_AIID: 2,
|
|
2780
|
+
/** piid carrying the work-mode selector value in the WORK action. */
|
|
2781
|
+
MODE_PIID: 1,
|
|
2782
|
+
/** piid carrying the point-info JSON in the WORK action. */
|
|
2783
|
+
POINT_PIID: 10,
|
|
2784
|
+
/** Work-mode selector values. */
|
|
2785
|
+
MODE_SPOT: 23,
|
|
2786
|
+
// go to point (MonitorSpotMode)
|
|
2787
|
+
MODE_CRUISE: 22,
|
|
2788
|
+
// cruise / patrol / find-pet
|
|
2789
|
+
MODE_PERSON_FOLLOW: 26,
|
|
2790
|
+
MODE_SPOT_CLEAN: 20,
|
|
2791
|
+
// spot-clean the current position (SpotClean)
|
|
2792
|
+
/** Auto-switch setter property (fill-light auto/full toggle lives here). */
|
|
2793
|
+
AUTO_SWITCH_PIID: 50
|
|
2794
|
+
};
|
|
2795
|
+
var VACUUM_CHARGE = { siid: 3, aiid: 1 };
|
|
2796
|
+
var VOICE_SIID = 7;
|
|
2797
|
+
var VOICE_LOCATE_AIID = 1;
|
|
2798
|
+
var VOICE_PLAY_SOUND_AIID = 2;
|
|
2799
|
+
var VOICE_SOUND_PIID = 4;
|
|
2800
|
+
var VACUUM_LOCATE = { siid: VOICE_SIID, aiid: VOICE_LOCATE_AIID };
|
|
2801
|
+
var PET_SOUNDS = {
|
|
2802
|
+
meow: 684,
|
|
2803
|
+
bark: 685,
|
|
2804
|
+
footsteps: 686,
|
|
2805
|
+
purring: 687,
|
|
2806
|
+
tickTock: 688
|
|
2807
|
+
};
|
|
2808
|
+
var VACUUM_ACTIONS = {
|
|
2809
|
+
startClean: { siid: 2, aiid: 1 },
|
|
2810
|
+
pauseClean: { siid: 2, aiid: 2 },
|
|
2811
|
+
stopClean: { siid: 4, aiid: 2 },
|
|
2812
|
+
dockWash: { siid: 4, aiid: 4 },
|
|
2813
|
+
autoEmpty: { siid: 15, aiid: 1 }
|
|
2814
|
+
};
|
|
2815
|
+
|
|
2816
|
+
// src/video/monitor/protocol.ts
|
|
2817
|
+
var import_node_crypto4 = require("crypto");
|
|
2818
|
+
function makeMonitorSession(accountId, now = Date.now()) {
|
|
2819
|
+
return (0, import_node_crypto4.createHash)("md5").update(`${accountId}_${now}`).digest("hex");
|
|
2820
|
+
}
|
|
2821
|
+
function hashAccessCode(code) {
|
|
2822
|
+
return (0, import_node_crypto4.createHash)("sha256").update(code).digest("hex");
|
|
2823
|
+
}
|
|
2824
|
+
function buildActionInput(piid, params, session) {
|
|
2825
|
+
const value = JSON.stringify({ ...params ?? {}, session });
|
|
2826
|
+
return { piid, value };
|
|
2827
|
+
}
|
|
2828
|
+
function startMonitorParams(iotId, vendor, area) {
|
|
2829
|
+
return {
|
|
2830
|
+
token: MONITOR_VENDOR_TOKEN[vendor],
|
|
2831
|
+
channelId: iotId,
|
|
2832
|
+
area,
|
|
2833
|
+
operType: "monitor",
|
|
2834
|
+
operation: "start"
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
function stopMonitorParams() {
|
|
2838
|
+
return { operType: "monitor", operation: "end" };
|
|
2839
|
+
}
|
|
2840
|
+
function keepAliveParams(videoStatus) {
|
|
2841
|
+
return { operType: "keep_alive", videoStatus };
|
|
2842
|
+
}
|
|
2843
|
+
function accessCodeLaunchParams() {
|
|
2844
|
+
return { open: true };
|
|
2845
|
+
}
|
|
2846
|
+
function verifyAccessCodeParams(code, lazymode = 0) {
|
|
2847
|
+
return { oldcode: hashAccessCode(code), lazymode };
|
|
2848
|
+
}
|
|
2849
|
+
function intercomStartParams(opts) {
|
|
2850
|
+
return {
|
|
2851
|
+
operType: "intercom",
|
|
2852
|
+
operation: "start",
|
|
2853
|
+
...opts?.needRecordSound !== void 0 ? { need_record_sound: opts.needRecordSound } : {},
|
|
2854
|
+
...opts?.videoCall ? { phone: 1 } : {}
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function intercomStopParams() {
|
|
2858
|
+
return { operType: "intercom", operation: "end" };
|
|
2859
|
+
}
|
|
2860
|
+
function fillLightParams(value) {
|
|
2861
|
+
return { value: String(value) };
|
|
2862
|
+
}
|
|
2863
|
+
function takePhotoParams() {
|
|
2864
|
+
return { operType: "takephoto", operation: "start" };
|
|
2865
|
+
}
|
|
2866
|
+
function remoteDriveValue(spdv, spdw, now = Date.now()) {
|
|
2867
|
+
return JSON.stringify({
|
|
2868
|
+
spdv,
|
|
2869
|
+
spdw,
|
|
2870
|
+
audio: "false",
|
|
2871
|
+
random: Math.floor(Math.random() * 1e3),
|
|
2872
|
+
timestamp: now
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
var DRIVE_DIRECTIONS = {
|
|
2876
|
+
forward: [200, 0],
|
|
2877
|
+
left: [0, 45],
|
|
2878
|
+
right: [0, -45],
|
|
2879
|
+
turnAround: [0, 180],
|
|
2880
|
+
stop: [0, 0]
|
|
2881
|
+
};
|
|
2882
|
+
function actionCode(res) {
|
|
2883
|
+
const r = res;
|
|
2884
|
+
return typeof r?.code === "number" ? r.code : null;
|
|
2885
|
+
}
|
|
2886
|
+
function firstOutValue(res) {
|
|
2887
|
+
const r = res;
|
|
2888
|
+
const out = r?.out ?? r?.result?.out;
|
|
2889
|
+
return Array.isArray(out) && out.length > 0 ? out[0]?.value : void 0;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// src/video/monitor/controller.ts
|
|
2893
|
+
var DEFAULT_KEEP_ALIVE_MS = 1e4;
|
|
2894
|
+
var DEFAULT_AREA = "4";
|
|
2895
|
+
var DreameCameraController = class {
|
|
2896
|
+
#device;
|
|
2897
|
+
#relay;
|
|
2898
|
+
#iotId;
|
|
2899
|
+
#accountId;
|
|
2900
|
+
#vendor;
|
|
2901
|
+
#accessCode;
|
|
2902
|
+
#area;
|
|
2903
|
+
#keepAliveIntervalMs;
|
|
2904
|
+
#wakeTimeoutMs;
|
|
2905
|
+
#onKeepAliveError;
|
|
2906
|
+
#log;
|
|
2907
|
+
#session = null;
|
|
2908
|
+
#keepAliveTimer = null;
|
|
2909
|
+
#opened = false;
|
|
2910
|
+
constructor(input) {
|
|
2911
|
+
this.#device = input.device;
|
|
2912
|
+
this.#relay = input.relay;
|
|
2913
|
+
this.#iotId = input.iotId;
|
|
2914
|
+
this.#accountId = input.accountId;
|
|
2915
|
+
this.#vendor = input.vendor ?? "ali";
|
|
2916
|
+
this.#accessCode = input.accessCode;
|
|
2917
|
+
this.#area = input.area ?? DEFAULT_AREA;
|
|
2918
|
+
this.#keepAliveIntervalMs = input.keepAliveIntervalMs ?? DEFAULT_KEEP_ALIVE_MS;
|
|
2919
|
+
this.#wakeTimeoutMs = input.wakeTimeoutMs;
|
|
2920
|
+
this.#onKeepAliveError = input.onKeepAliveError;
|
|
2921
|
+
this.#log = input.log ?? (() => {
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
#logLine(msg) {
|
|
2925
|
+
this.#log(`[nodedreame:monitor] ${msg}`);
|
|
2926
|
+
}
|
|
2927
|
+
/** True while a monitor session is active (between {@link open} and {@link close}). */
|
|
2928
|
+
get isOpen() {
|
|
2929
|
+
return this.#opened;
|
|
2930
|
+
}
|
|
2931
|
+
/** Send one camera action, resolving to the raw device reply. */
|
|
2932
|
+
async #action(aiid, piid, params) {
|
|
2933
|
+
if (!this.#session) {
|
|
2934
|
+
throw new DreameError("camera controller has no session; call open() first");
|
|
2935
|
+
}
|
|
2936
|
+
const input = buildActionInput(piid, params, this.#session);
|
|
2937
|
+
return this.#device.callAction(MONITOR_SIID, aiid, [input]);
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Run the full cold-start sequence and return a live relay URL. Starts the
|
|
2941
|
+
* keep-alive loop; the caller MUST {@link close} when done to release the
|
|
2942
|
+
* device. Throws if the monitor cannot be started or no relay comes up.
|
|
2943
|
+
*/
|
|
2944
|
+
async open() {
|
|
2945
|
+
if (this.#opened) {
|
|
2946
|
+
throw new DreameError("camera stream already open");
|
|
2947
|
+
}
|
|
2948
|
+
this.#session = makeMonitorSession(this.#accountId);
|
|
2949
|
+
this.#logLine(`open: vendor=${this.#vendor} iotId=${this.#iotId} area=${this.#area}`);
|
|
2950
|
+
await this.#tryPrime();
|
|
2951
|
+
const startRes = await this.#startMonitorWithGate();
|
|
2952
|
+
const encryptionKey = asString(firstOutValue(startRes));
|
|
2953
|
+
this.#logLine(`startMonitor accepted (encryptionKey=${encryptionKey ? "yes" : "no"})`);
|
|
2954
|
+
this.#opened = true;
|
|
2955
|
+
this.#startKeepAlive();
|
|
2956
|
+
let relayUrl;
|
|
2957
|
+
try {
|
|
2958
|
+
const info = await this.#relay.getStreamInfo(this.#iotId, {
|
|
2959
|
+
encrypted: false,
|
|
2960
|
+
...this.#wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: this.#wakeTimeoutMs } : {}
|
|
2961
|
+
});
|
|
2962
|
+
relayUrl = info.relayUrl;
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
this.#logLine(`relay mint failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2965
|
+
await this.close();
|
|
2966
|
+
throw err;
|
|
2967
|
+
}
|
|
2968
|
+
if (!relayUrl) {
|
|
2969
|
+
await this.close();
|
|
2970
|
+
throw new DreameError("monitor started but no relay URL was returned");
|
|
2971
|
+
}
|
|
2972
|
+
this.#logLine("relay up");
|
|
2973
|
+
return { rtmpUrl: relayUrl, encryptionKey };
|
|
2974
|
+
}
|
|
2975
|
+
async #tryPrime() {
|
|
2976
|
+
try {
|
|
2977
|
+
await this.#action(MONITOR_AIID.ACCESS_CODE_OPERATE, MONITOR_PIID.GET_ACCESS_CODE, accessCodeLaunchParams());
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2980
|
+
try {
|
|
2981
|
+
await this.#action(MONITOR_AIID.VIDEO_VENDOR, MONITOR_PIID.VIDEO_VENDOR_STATUS, null);
|
|
2982
|
+
} catch {
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
async #startMonitorWithGate() {
|
|
2986
|
+
const params = startMonitorParams(this.#iotId, this.#vendor, this.#area);
|
|
2987
|
+
let res = await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, params);
|
|
2988
|
+
if (actionCode(res) === 0) {
|
|
2989
|
+
return res;
|
|
2990
|
+
}
|
|
2991
|
+
this.#logLine(`startMonitor refused (code ${actionCode(res) ?? "?"}); verifying access code`);
|
|
2992
|
+
if (this.#accessCode) {
|
|
2993
|
+
await this.#action(
|
|
2994
|
+
MONITOR_AIID.ACCESS_CODE_OPERATE,
|
|
2995
|
+
MONITOR_PIID.VERIFY_ACCESS_CODE,
|
|
2996
|
+
verifyAccessCodeParams(this.#accessCode)
|
|
2997
|
+
);
|
|
2998
|
+
res = await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, params);
|
|
2999
|
+
}
|
|
3000
|
+
if (actionCode(res) !== 0) {
|
|
3001
|
+
const code = actionCode(res);
|
|
3002
|
+
throw new DreameError(
|
|
3003
|
+
`startMonitor refused (code ${code ?? "?"})` + (this.#accessCode ? "" : "; device likely requires an accessCode")
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
return res;
|
|
3007
|
+
}
|
|
3008
|
+
#startKeepAlive() {
|
|
3009
|
+
this.#stopKeepAlive();
|
|
3010
|
+
this.#keepAliveTimer = setInterval(() => {
|
|
3011
|
+
void this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.KEEP_ALIVE, keepAliveParams("opened")).catch(
|
|
3012
|
+
(err) => this.#onKeepAliveError?.(err)
|
|
3013
|
+
);
|
|
3014
|
+
}, this.#keepAliveIntervalMs);
|
|
3015
|
+
this.#keepAliveTimer.unref?.();
|
|
3016
|
+
}
|
|
3017
|
+
#stopKeepAlive() {
|
|
3018
|
+
if (this.#keepAliveTimer) {
|
|
3019
|
+
clearInterval(this.#keepAliveTimer);
|
|
3020
|
+
this.#keepAliveTimer = null;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Mint a FRESH single-use RTMP relay URL for the already-open monitor. Each
|
|
3025
|
+
* consumer needs its own relay URL, so call this once per ffmpeg/consumer.
|
|
3026
|
+
* Throws if the stream is not open.
|
|
3027
|
+
*/
|
|
3028
|
+
async mintRelayUrl() {
|
|
3029
|
+
if (!this.#opened) {
|
|
3030
|
+
throw new DreameError("camera stream is not open; call open() first");
|
|
3031
|
+
}
|
|
3032
|
+
const info = await this.#relay.getStreamInfo(this.#iotId, {
|
|
3033
|
+
encrypted: false,
|
|
3034
|
+
...this.#wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: this.#wakeTimeoutMs } : {}
|
|
3035
|
+
});
|
|
3036
|
+
if (!info.relayUrl) {
|
|
3037
|
+
throw new DreameError("no relay URL available for the open monitor");
|
|
3038
|
+
}
|
|
3039
|
+
return info.relayUrl;
|
|
3040
|
+
}
|
|
3041
|
+
/** Start the two-way intercom session (control plane only; see module docs on mic uplink). */
|
|
3042
|
+
async startIntercom(opts) {
|
|
3043
|
+
return this.#action(MONITOR_AIID.VOICE_OPERATE, MONITOR_PIID.MONITOR_AUDIO_STATUS, intercomStartParams(opts));
|
|
3044
|
+
}
|
|
3045
|
+
/** Stop the two-way intercom session. */
|
|
3046
|
+
async stopIntercom() {
|
|
3047
|
+
return this.#action(MONITOR_AIID.VOICE_OPERATE, MONITOR_PIID.MONITOR_AUDIO_STATUS, intercomStopParams());
|
|
3048
|
+
}
|
|
3049
|
+
/** Set the fill-light brightness (40–100 = manual %, below 40 / 101 = auto). */
|
|
3050
|
+
async setFillLight(value) {
|
|
3051
|
+
return this.#action(MONITOR_AIID.PROPERTY_OPERATE, MONITOR_PIID.FILL_LIGHT_SET, fillLightParams(value));
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Device-side snapshot: the robot captures a still and uploads it to cloud
|
|
3055
|
+
* storage (read the result back from the `uploadStatus` property). Prefer a
|
|
3056
|
+
* frame-grab from the live stream for an instant image.
|
|
3057
|
+
*/
|
|
3058
|
+
async takePhoto() {
|
|
3059
|
+
return this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.TAKE_PHOTO, takePhotoParams());
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Remote-drive the robot ("PTZ" substitute — the camera is fixed, the robot
|
|
3063
|
+
* moves). `spdv` = forward speed, `spdw` = turn; send repeatedly (~1 Hz) while
|
|
3064
|
+
* a direction is held, then `stop`. Only valid while the stream is open.
|
|
3065
|
+
*/
|
|
3066
|
+
async drive(spdv, spdw) {
|
|
3067
|
+
this.#requireSession();
|
|
3068
|
+
if (!this.#device.setProperty) {
|
|
3069
|
+
throw new DreameError("device does not support setProperty; cannot remote-drive");
|
|
3070
|
+
}
|
|
3071
|
+
return this.#device.setProperty({
|
|
3072
|
+
siid: VACUUM_SIID,
|
|
3073
|
+
piid: VACUUM_MOVE.REMOTE_STATE_PIID,
|
|
3074
|
+
value: remoteDriveValue(spdv, spdw)
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
/** Remote-drive by named direction (forward/left/right/turnAround/stop). */
|
|
3078
|
+
async driveDirection(direction) {
|
|
3079
|
+
const [spdv, spdw] = DRIVE_DIRECTIONS[direction];
|
|
3080
|
+
return this.drive(spdv, spdw);
|
|
3081
|
+
}
|
|
3082
|
+
/** Start person-follow mode (robot tracks a detected person). */
|
|
3083
|
+
async startPersonFollow() {
|
|
3084
|
+
this.#requireSession();
|
|
3085
|
+
return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
|
|
3086
|
+
{ piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_PERSON_FOLLOW }
|
|
3087
|
+
]);
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Universal stop for the current work mode — stops person-follow, spot-clean,
|
|
3091
|
+
* go-to-point, cruise, etc. (siid 4 / aiid 2).
|
|
3092
|
+
*/
|
|
3093
|
+
async stopWork() {
|
|
3094
|
+
return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.STOP_AIID, []);
|
|
3095
|
+
}
|
|
3096
|
+
/** Stop person-follow mode (alias for {@link stopWork}). */
|
|
3097
|
+
async stopPersonFollow() {
|
|
3098
|
+
return this.stopWork();
|
|
3099
|
+
}
|
|
3100
|
+
/** Spot-clean the robot's current position (WorkMode SpotClean). */
|
|
3101
|
+
async spotClean() {
|
|
3102
|
+
return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
|
|
3103
|
+
{ piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_SPOT_CLEAN }
|
|
3104
|
+
]);
|
|
3105
|
+
}
|
|
3106
|
+
/** Play one of the robot's sound clips (e.g. pet sounds) by id. */
|
|
3107
|
+
async playSound(soundId) {
|
|
3108
|
+
return this.#device.callAction(VOICE_SIID, VOICE_PLAY_SOUND_AIID, [
|
|
3109
|
+
{ piid: VOICE_SOUND_PIID, value: soundId }
|
|
3110
|
+
]);
|
|
3111
|
+
}
|
|
3112
|
+
/** Play a named pet-teasing sound (meow/bark/footsteps/purring/tickTock). */
|
|
3113
|
+
async playPetSound(sound) {
|
|
3114
|
+
return this.playSound(PET_SOUNDS[sound]);
|
|
3115
|
+
}
|
|
3116
|
+
/** Run a common whole-robot action (startClean/pauseClean/stopClean/dockWash/autoEmpty). */
|
|
3117
|
+
async runVacuumAction(action) {
|
|
3118
|
+
const a = VACUUM_ACTIONS[action];
|
|
3119
|
+
return this.#device.callAction(a.siid, a.aiid, []);
|
|
3120
|
+
}
|
|
3121
|
+
/** Find-pet: cruise the home looking for the pet. */
|
|
3122
|
+
async findPet() {
|
|
3123
|
+
this.#requireSession();
|
|
3124
|
+
return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
|
|
3125
|
+
{ piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_CRUISE },
|
|
3126
|
+
{ piid: VACUUM_MOVE.POINT_PIID, value: JSON.stringify({ findpet: "" }) }
|
|
3127
|
+
]);
|
|
3128
|
+
}
|
|
3129
|
+
/** Send the robot to a map point (`spoint` current, `tpoint` targets). */
|
|
3130
|
+
async goToPoint(spoint, tpoint) {
|
|
3131
|
+
this.#requireSession();
|
|
3132
|
+
return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
|
|
3133
|
+
{ piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_SPOT },
|
|
3134
|
+
{ piid: VACUUM_MOVE.POINT_PIID, value: JSON.stringify({ spoint, tpoint }) }
|
|
3135
|
+
]);
|
|
3136
|
+
}
|
|
3137
|
+
/** Toggle the fill light between auto (`full=false`) and full-on (`full=true`). */
|
|
3138
|
+
async setFillLightAuto(full) {
|
|
3139
|
+
if (!this.#device.setProperty) {
|
|
3140
|
+
throw new DreameError("device does not support setProperty; cannot toggle fill light");
|
|
3141
|
+
}
|
|
3142
|
+
return this.#device.setProperty({
|
|
3143
|
+
siid: VACUUM_SIID,
|
|
3144
|
+
piid: VACUUM_MOVE.AUTO_SWITCH_PIID,
|
|
3145
|
+
value: JSON.stringify({ k: "FillinLight", v: full ? 1 : 0 })
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Send the robot back to its dock to charge ("go home"). Works regardless of
|
|
3150
|
+
* stream state — usable as a standalone command or a PTZ "home" preset.
|
|
3151
|
+
*/
|
|
3152
|
+
async returnToDock() {
|
|
3153
|
+
return this.#device.callAction(VACUUM_CHARGE.siid, VACUUM_CHARGE.aiid, []);
|
|
3154
|
+
}
|
|
3155
|
+
/** Locate the robot — it beeps. Works regardless of stream state. */
|
|
3156
|
+
async locate() {
|
|
3157
|
+
return this.#device.callAction(VACUUM_LOCATE.siid, VACUUM_LOCATE.aiid, []);
|
|
3158
|
+
}
|
|
3159
|
+
#requireSession() {
|
|
3160
|
+
if (!this.#opened) {
|
|
3161
|
+
throw new DreameError("camera stream is not open; call open() first");
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
/** Stop the keep-alive loop and release the monitor on the device. Idempotent. */
|
|
3165
|
+
async close() {
|
|
3166
|
+
this.#stopKeepAlive();
|
|
3167
|
+
if (!this.#opened) {
|
|
3168
|
+
this.#session = null;
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
this.#opened = false;
|
|
3172
|
+
this.#logLine("close: stopping monitor");
|
|
3173
|
+
try {
|
|
3174
|
+
await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, stopMonitorParams());
|
|
3175
|
+
} finally {
|
|
3176
|
+
this.#session = null;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
function asString(v) {
|
|
3181
|
+
return typeof v === "string" ? v : null;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
2017
3184
|
// src/models/vacuum/map/envelope.ts
|
|
2018
|
-
var
|
|
3185
|
+
var import_node_crypto5 = require("crypto");
|
|
2019
3186
|
var zlib = __toESM(require("zlib"), 1);
|
|
2020
3187
|
var HEADER_SIZE = 27;
|
|
2021
3188
|
var ANGLE_ABSENT = 32767;
|
|
@@ -2093,7 +3260,7 @@ function looksLikeBase64Zlib(buf) {
|
|
|
2093
3260
|
}
|
|
2094
3261
|
function aesCbcDecrypt(cipher, rawKey, iv) {
|
|
2095
3262
|
const keyBytes = Buffer.from(
|
|
2096
|
-
(0,
|
|
3263
|
+
(0, import_node_crypto5.createHash)("sha256").update(rawKey).digest("hex").slice(0, 32),
|
|
2097
3264
|
"utf8"
|
|
2098
3265
|
);
|
|
2099
3266
|
const ivBytes = Buffer.from(iv, "utf8");
|
|
@@ -2101,7 +3268,7 @@ function aesCbcDecrypt(cipher, rawKey, iv) {
|
|
|
2101
3268
|
throw new MapDecodeError(`envelope: AES IV must be 16 ASCII bytes, got ${ivBytes.length}`);
|
|
2102
3269
|
}
|
|
2103
3270
|
try {
|
|
2104
|
-
const decipher = (0,
|
|
3271
|
+
const decipher = (0, import_node_crypto5.createDecipheriv)("aes-256-cbc", keyBytes, ivBytes);
|
|
2105
3272
|
decipher.setAutoPadding(false);
|
|
2106
3273
|
return Buffer.concat([decipher.update(cipher), decipher.final()]);
|
|
2107
3274
|
} catch (err) {
|
|
@@ -3434,6 +4601,97 @@ function drawLabel(png, cx, cy, text, color, px) {
|
|
|
3434
4601
|
}
|
|
3435
4602
|
}
|
|
3436
4603
|
|
|
4604
|
+
// src/models/vacuum/map/wifi.ts
|
|
4605
|
+
var import_pngjs2 = require("pngjs");
|
|
4606
|
+
var NIBBLE_TO_BARS = { 10: 0, 11: 1, 12: 2, 13: 3, 14: 4 };
|
|
4607
|
+
var isWifiNibble = (n) => n >= 10 && n <= 14;
|
|
4608
|
+
var NIBBLE_RGBA = {
|
|
4609
|
+
2: [176, 182, 190, 255],
|
|
4610
|
+
// map border (context)
|
|
4611
|
+
3: [242, 244, 247, 255],
|
|
4612
|
+
// mapped floor (context)
|
|
4613
|
+
10: [229, 234, 238, 255],
|
|
4614
|
+
// unreached
|
|
4615
|
+
11: [217, 226, 239, 255],
|
|
4616
|
+
// 1 bar
|
|
4617
|
+
12: [205, 218, 239, 255],
|
|
4618
|
+
// 2 bars
|
|
4619
|
+
13: [161, 189, 242, 255],
|
|
4620
|
+
// 3 bars
|
|
4621
|
+
14: [129, 168, 245, 255]
|
|
4622
|
+
// 4 bars
|
|
4623
|
+
};
|
|
4624
|
+
var poseOrNull = (x, y, a) => x === 0 && y === 0 ? null : { x, y, angle: a === ANGLE_ABSENT ? 0 : a };
|
|
4625
|
+
function decodeWifiSignalMap(input, opts = {}) {
|
|
4626
|
+
const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
|
|
4627
|
+
const header = parseMapHeader(inflated);
|
|
4628
|
+
const { width, height, gridSize, left, top } = header;
|
|
4629
|
+
const pixelEnd = HEADER_SIZE + width * height;
|
|
4630
|
+
const grid = inflated.subarray(HEADER_SIZE, pixelEnd);
|
|
4631
|
+
const cells = new Uint8Array(width * height);
|
|
4632
|
+
for (let i = 0; i < cells.length; i++) cells[i] = (grid[i] ?? 0) & 15;
|
|
4633
|
+
const dimensions = { left, top, width, height, gridSize };
|
|
4634
|
+
const robot = poseOrNull(header.robotX, header.robotY, header.robotA);
|
|
4635
|
+
const dock = poseOrNull(header.chargerX, header.chargerY, header.chargerA);
|
|
4636
|
+
const signalAt = (worldX, worldY) => {
|
|
4637
|
+
if (gridSize <= 0) return null;
|
|
4638
|
+
const col = Math.floor((worldX - left) / gridSize);
|
|
4639
|
+
const row = Math.floor((worldY - top) / gridSize);
|
|
4640
|
+
if (col < 0 || col >= width || row < 0 || row >= height) return null;
|
|
4641
|
+
const nibble = cells[row * width + col] ?? 0;
|
|
4642
|
+
return isWifiNibble(nibble) ? NIBBLE_TO_BARS[nibble] : null;
|
|
4643
|
+
};
|
|
4644
|
+
return {
|
|
4645
|
+
dimensions,
|
|
4646
|
+
robot,
|
|
4647
|
+
dock,
|
|
4648
|
+
cells,
|
|
4649
|
+
signalAt,
|
|
4650
|
+
currentSignal: robot ? signalAt(robot.x, robot.y) : null
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
function renderWifiSignalPng(map, opts = {}) {
|
|
4654
|
+
const scale = Math.max(1, Math.floor(opts.scale ?? 4));
|
|
4655
|
+
const { width, height, left, top, gridSize } = map.dimensions;
|
|
4656
|
+
const png = new import_pngjs2.PNG({ width: width * scale, height: height * scale, colorType: 6 });
|
|
4657
|
+
for (let row = 0; row < height; row++) {
|
|
4658
|
+
for (let col = 0; col < width; col++) {
|
|
4659
|
+
const nibble = map.cells[row * width + col] ?? 0;
|
|
4660
|
+
const rgba = NIBBLE_RGBA[nibble];
|
|
4661
|
+
if (!rgba) continue;
|
|
4662
|
+
for (let dy = 0; dy < scale; dy++) {
|
|
4663
|
+
for (let dx = 0; dx < scale; dx++) {
|
|
4664
|
+
const px = col * scale + dx + (row * scale + dy) * width * scale;
|
|
4665
|
+
const o = px << 2;
|
|
4666
|
+
png.data[o] = rgba[0];
|
|
4667
|
+
png.data[o + 1] = rgba[1];
|
|
4668
|
+
png.data[o + 2] = rgba[2];
|
|
4669
|
+
png.data[o + 3] = rgba[3];
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
if ((opts.markRobot ?? true) && map.robot && gridSize > 0) {
|
|
4675
|
+
const cx = Math.floor((map.robot.x - left) / gridSize) * scale + Math.floor(scale / 2);
|
|
4676
|
+
const cy = Math.floor((map.robot.y - top) / gridSize) * scale + Math.floor(scale / 2);
|
|
4677
|
+
const r = Math.max(2, scale);
|
|
4678
|
+
for (let dy = -r; dy <= r; dy++) {
|
|
4679
|
+
for (let dx = -r; dx <= r; dx++) {
|
|
4680
|
+
if (dx * dx + dy * dy > r * r) continue;
|
|
4681
|
+
const x = cx + dx;
|
|
4682
|
+
const y = cy + dy;
|
|
4683
|
+
if (x < 0 || y < 0 || x >= width * scale || y >= height * scale) continue;
|
|
4684
|
+
const o = x + y * width * scale << 2;
|
|
4685
|
+
png.data[o] = 255;
|
|
4686
|
+
png.data[o + 1] = 69;
|
|
4687
|
+
png.data[o + 2] = 0;
|
|
4688
|
+
png.data[o + 3] = 255;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
return import_pngjs2.PNG.sync.write(png);
|
|
4693
|
+
}
|
|
4694
|
+
|
|
3437
4695
|
// src/models/vacuum/map/oss-fetch.ts
|
|
3438
4696
|
var OSS_DOWNLOAD_PATH = "/dreame-user-iot/iotfile/getDownloadUrl";
|
|
3439
4697
|
var DEFAULT_TTL_MS = 30 * 60 * 1e3;
|
|
@@ -3919,11 +5177,11 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
3919
5177
|
}
|
|
3920
5178
|
this.#requireCap(this.#caps.canCleanPerRoom, "cleanZones", "per-room cleaning");
|
|
3921
5179
|
const { repeats, fan, water } = this.#resolveCleanOpts(opts);
|
|
3922
|
-
const areas = zones.map((
|
|
3923
|
-
Math.round(
|
|
3924
|
-
Math.round(
|
|
3925
|
-
Math.round(
|
|
3926
|
-
Math.round(
|
|
5180
|
+
const areas = zones.map((z7) => [
|
|
5181
|
+
Math.round(z7.x0),
|
|
5182
|
+
Math.round(z7.y0),
|
|
5183
|
+
Math.round(z7.x1),
|
|
5184
|
+
Math.round(z7.y1),
|
|
3927
5185
|
repeats,
|
|
3928
5186
|
fan,
|
|
3929
5187
|
water
|
|
@@ -4051,6 +5309,66 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
4051
5309
|
...input.iv !== void 0 ? { iv: input.iv } : {}
|
|
4052
5310
|
};
|
|
4053
5311
|
}
|
|
5312
|
+
/**
|
|
5313
|
+
* Fetch the robot's Wi-Fi signal map — the last stored coverage heatmap, with
|
|
5314
|
+
* NO robot movement. Requests it (map service `wifiMap` action, siid 6 aiid 4),
|
|
5315
|
+
* polls `PropWifiMap` (6/15) for the OSS object it advertises, fetches + decodes
|
|
5316
|
+
* it into a per-cell signal grid + geometry + robot/dock poses. The signal at
|
|
5317
|
+
* any world point (incl. the robot's own position) is a lookup — see
|
|
5318
|
+
* {@link WifiSignalMap.signalAt} / {@link WifiSignalMap.currentSignal}.
|
|
5319
|
+
*
|
|
5320
|
+
* Pass `requestFresh: false` to skip the request and read whatever is already
|
|
5321
|
+
* advertised. Throws if no Wi-Fi map exists yet (never run on this device).
|
|
5322
|
+
*/
|
|
5323
|
+
async getWifiSignalMap(input = {}) {
|
|
5324
|
+
if (input.requestFresh !== false) {
|
|
5325
|
+
try {
|
|
5326
|
+
await this.callAction(6, 4, []);
|
|
5327
|
+
} catch {
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
const pollMs = input.pollMs ?? 2500;
|
|
5331
|
+
const deadline = Date.now() + (input.maxWaitMs ?? 2e4);
|
|
5332
|
+
let objectName = "";
|
|
5333
|
+
for (; ; ) {
|
|
5334
|
+
const res = await this.refreshProperties([{ siid: 6, piid: 15 }]);
|
|
5335
|
+
const val = res[0]?.value;
|
|
5336
|
+
if (typeof val === "string" && val.length > 0) {
|
|
5337
|
+
objectName = val;
|
|
5338
|
+
break;
|
|
5339
|
+
}
|
|
5340
|
+
if (Date.now() + pollMs >= deadline) {
|
|
5341
|
+
throw new DreameError("wifi signal map not available (none stored yet, or request timed out)");
|
|
5342
|
+
}
|
|
5343
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
5344
|
+
}
|
|
5345
|
+
const filename = objectName.replace(/^\d+,/, "");
|
|
5346
|
+
const blob = await this.#fetchMapBlob({
|
|
5347
|
+
filename,
|
|
5348
|
+
...input.fetcher ? { fetcher: input.fetcher } : {},
|
|
5349
|
+
...input.host ? { host: input.host } : {},
|
|
5350
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
|
|
5351
|
+
...input.signal ? { signal: input.signal } : {}
|
|
5352
|
+
});
|
|
5353
|
+
return decodeWifiSignalMap(blob, this.#decodeOpts(input));
|
|
5354
|
+
}
|
|
5355
|
+
/**
|
|
5356
|
+
* Render the Wi-Fi signal map as a simple PNG heatmap image (weakest→strongest,
|
|
5357
|
+
* unreached grey, robot marker) — ready to display. Convenience over
|
|
5358
|
+
* {@link getWifiSignalMap} + {@link renderWifiSignalPng}.
|
|
5359
|
+
*/
|
|
5360
|
+
async getWifiSignalImage(input = {}) {
|
|
5361
|
+
const map = await this.getWifiSignalMap(input);
|
|
5362
|
+
return renderWifiSignalPng(map, input);
|
|
5363
|
+
}
|
|
5364
|
+
/**
|
|
5365
|
+
* The one-call "current Wi-Fi signal": fetches the signal map and returns the
|
|
5366
|
+
* bars (1–4, 0 = unreached) at the robot's own position, or null when unknown.
|
|
5367
|
+
*/
|
|
5368
|
+
async getCurrentSignal(input = {}) {
|
|
5369
|
+
const map = await this.getWifiSignalMap(input);
|
|
5370
|
+
return map.currentSignal;
|
|
5371
|
+
}
|
|
4054
5372
|
/**
|
|
4055
5373
|
* The merge base for live P-frame streaming — always an INFLATED frame buffer.
|
|
4056
5374
|
* An I-frame (re)seeds it; each P-frame merge replaces it with the merged
|
|
@@ -4114,6 +5432,39 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
4114
5432
|
this.emit("map", frame);
|
|
4115
5433
|
return frame;
|
|
4116
5434
|
}
|
|
5435
|
+
/**
|
|
5436
|
+
* Build a {@link DreameCameraController} for this device's camera, wired to
|
|
5437
|
+
* both planes it needs: the MIoT action transport (this device handle) and a
|
|
5438
|
+
* fresh Aliyun {@link DreameVideoSession} for the relay. Call
|
|
5439
|
+
* `controller.open()` to autonomously cold-start the stream and get a pullable
|
|
5440
|
+
* RTMP URL, then `controller.close()` to release it.
|
|
5441
|
+
*
|
|
5442
|
+
* The camera's per-session privacy gate means a coded camera needs its PIN:
|
|
5443
|
+
* pass `accessCode` (the code set at pairing). `iotId` is resolved from the
|
|
5444
|
+
* device's cloud video profile when not supplied.
|
|
5445
|
+
*/
|
|
5446
|
+
async createCameraController(opts) {
|
|
5447
|
+
const session = this.currentSession();
|
|
5448
|
+
const region = this.region;
|
|
5449
|
+
const relay = opts?.relay ?? new DreameVideoSession({ session, region });
|
|
5450
|
+
const iotId = opts?.iotId ?? (await getDeviceVideoProfile({ session, region, did: this.deviceId })).iotId;
|
|
5451
|
+
if (!iotId) {
|
|
5452
|
+
throw new DreameError("device has no LinkVisual iotId; not a camera device or not provisioned");
|
|
5453
|
+
}
|
|
5454
|
+
return new DreameCameraController({
|
|
5455
|
+
device: this,
|
|
5456
|
+
relay,
|
|
5457
|
+
iotId,
|
|
5458
|
+
accountId: session.uid,
|
|
5459
|
+
...opts?.vendor ? { vendor: opts.vendor } : {},
|
|
5460
|
+
...opts?.accessCode !== void 0 ? { accessCode: opts.accessCode } : {},
|
|
5461
|
+
...opts?.area ? { area: opts.area } : {},
|
|
5462
|
+
...opts?.keepAliveIntervalMs !== void 0 ? { keepAliveIntervalMs: opts.keepAliveIntervalMs } : {},
|
|
5463
|
+
...opts?.wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: opts.wakeTimeoutMs } : {},
|
|
5464
|
+
...opts?.onKeepAliveError ? { onKeepAliveError: opts.onKeepAliveError } : {},
|
|
5465
|
+
...opts?.log ? { log: opts.log } : {}
|
|
5466
|
+
});
|
|
5467
|
+
}
|
|
4117
5468
|
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
4118
5469
|
static DEFAULT_PROPS = [
|
|
4119
5470
|
VACUUM_PROP.STATE,
|
|
@@ -4652,7 +6003,7 @@ function isRecord3(v) {
|
|
|
4652
6003
|
function asNumber(v, fallback) {
|
|
4653
6004
|
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
4654
6005
|
}
|
|
4655
|
-
function
|
|
6006
|
+
function asString2(v, fallback) {
|
|
4656
6007
|
return typeof v === "string" ? v : fallback;
|
|
4657
6008
|
}
|
|
4658
6009
|
function mapIdFromIndex(mapIndex) {
|
|
@@ -4747,7 +6098,7 @@ function parseZones(list, includeTimes) {
|
|
|
4747
6098
|
out.push({
|
|
4748
6099
|
zoneId: asNumber(id, 0),
|
|
4749
6100
|
path: extractPathCoords(data["path"]),
|
|
4750
|
-
name:
|
|
6101
|
+
name: asString2(data["name"], ""),
|
|
4751
6102
|
zoneType: asNumber(data["type"], 0),
|
|
4752
6103
|
shapeType: includeTimes ? asNumber(data["shapeType"], 0) : 0,
|
|
4753
6104
|
area: includeTimes ? asNumber(data["area"], 0) : 0,
|
|
@@ -4768,7 +6119,7 @@ function parseSpotAreas(list) {
|
|
|
4768
6119
|
out.push({
|
|
4769
6120
|
areaId: Math.trunc(asNumber(id, 0)),
|
|
4770
6121
|
path: extractPathCoords(data["path"]),
|
|
4771
|
-
name:
|
|
6122
|
+
name: asString2(data["name"], ""),
|
|
4772
6123
|
shapeType: asNumber(data["shapeType"], 0),
|
|
4773
6124
|
area: asNumber(data["area"], 0)
|
|
4774
6125
|
});
|
|
@@ -4832,7 +6183,7 @@ function parseMowerMap(mapJsonStr) {
|
|
|
4832
6183
|
contours: parseContours(parsePolygonList(data["contours"])),
|
|
4833
6184
|
boundary: parseBoundary(data["boundary"]),
|
|
4834
6185
|
totalArea: asNumber(data["totalArea"], 0),
|
|
4835
|
-
name:
|
|
6186
|
+
name: asString2(data["name"], ""),
|
|
4836
6187
|
mapIndex,
|
|
4837
6188
|
mapId: mapIdFromIndex(mapIndex),
|
|
4838
6189
|
mowPaths: [],
|
|
@@ -5360,7 +6711,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
|
|
|
5360
6711
|
if (zoneIds.length === 0) {
|
|
5361
6712
|
throw new RangeError("startMowingZones: zoneIds must not be empty");
|
|
5362
6713
|
}
|
|
5363
|
-
return this.#sendTask(buildZonePayload(zoneIds.map((
|
|
6714
|
+
return this.#sendTask(buildZonePayload(zoneIds.map((z7) => Math.trunc(z7))));
|
|
5364
6715
|
}
|
|
5365
6716
|
/** Edge / contour mowing (2:50 o:101). Contour ids are two-int pairs [[1,0]]. */
|
|
5366
6717
|
async startMowingEdges(contourIds) {
|
|
@@ -5525,6 +6876,7 @@ function defaultDeps(opts) {
|
|
|
5525
6876
|
device: args.device,
|
|
5526
6877
|
region: args.region,
|
|
5527
6878
|
sessionRef: args.sessionRef,
|
|
6879
|
+
...args.onAuthFailure !== void 0 ? { onAuthFailure: args.onAuthFailure } : {},
|
|
5528
6880
|
...opts.fetchInitialValues !== void 0 ? { fetchInitialValues: opts.fetchInitialValues } : {},
|
|
5529
6881
|
...opts.pollIntervalMs !== void 0 ? { pollIntervalMs: opts.pollIntervalMs } : {}
|
|
5530
6882
|
});
|
|
@@ -5538,6 +6890,15 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5538
6890
|
#session = null;
|
|
5539
6891
|
#devices = [];
|
|
5540
6892
|
#closed = false;
|
|
6893
|
+
/** Background timer that proactively refreshes before the token expires. */
|
|
6894
|
+
#refreshTimer = null;
|
|
6895
|
+
/** Guard against overlapping proactive-refresh runs. */
|
|
6896
|
+
#refreshInFlight = false;
|
|
6897
|
+
/**
|
|
6898
|
+
* The single in-flight refresh, shared by concurrent callers so the proactive
|
|
6899
|
+
* timer and a manual `ensureSession()`/`reauthenticate()` never double-refresh.
|
|
6900
|
+
*/
|
|
6901
|
+
#inFlightRefresh = null;
|
|
5541
6902
|
constructor(opts, deps) {
|
|
5542
6903
|
super();
|
|
5543
6904
|
if (!opts.username || !opts.password) {
|
|
@@ -5566,6 +6927,7 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5566
6927
|
...this.#opts.lang !== void 0 ? { lang: this.#opts.lang } : {},
|
|
5567
6928
|
...this.#opts.fetchImpl !== void 0 ? { fetchImpl: this.#opts.fetchImpl } : {}
|
|
5568
6929
|
});
|
|
6930
|
+
this.#scheduleProactiveRefresh();
|
|
5569
6931
|
return this.#session;
|
|
5570
6932
|
}
|
|
5571
6933
|
/** Return a valid session, refreshing proactively within the leeway window. */
|
|
@@ -5577,18 +6939,61 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5577
6939
|
if (Date.now() < current.expiresAt - this.#leewayMs) {
|
|
5578
6940
|
return current;
|
|
5579
6941
|
}
|
|
6942
|
+
return this.#refreshNow(current);
|
|
6943
|
+
}
|
|
6944
|
+
/**
|
|
6945
|
+
* Force a session refresh REGARDLESS of the current expiry, then propagate the
|
|
6946
|
+
* new token to every live device push. Wired as each push's `onAuthFailure`:
|
|
6947
|
+
* when a broker refuses a CONNECT because it considers the token stale (even
|
|
6948
|
+
* though {@link ensureSession} still thinks it valid — e.g. server-side early
|
|
6949
|
+
* revocation or clock skew), this mints a genuinely fresh token instead of
|
|
6950
|
+
* replaying the rejected one.
|
|
6951
|
+
*/
|
|
6952
|
+
async reauthenticate() {
|
|
6953
|
+
if (this.#closed) {
|
|
6954
|
+
throw new DreameAuthError("client is closed");
|
|
6955
|
+
}
|
|
6956
|
+
const current = this.#session;
|
|
6957
|
+
if (!current) {
|
|
6958
|
+
return this.login();
|
|
6959
|
+
}
|
|
6960
|
+
return this.#refreshNow(current);
|
|
6961
|
+
}
|
|
6962
|
+
/**
|
|
6963
|
+
* Refresh via the refresh-token (with a full re-login fallback) and propagate
|
|
6964
|
+
* the new token to every device push. Shared by {@link ensureSession} (within
|
|
6965
|
+
* the leeway window) and {@link reauthenticate} (unconditional).
|
|
6966
|
+
*/
|
|
6967
|
+
async #refreshNow(current) {
|
|
6968
|
+
const existing = this.#inFlightRefresh;
|
|
6969
|
+
if (existing) {
|
|
6970
|
+
return existing;
|
|
6971
|
+
}
|
|
6972
|
+
const run = this.#doRefresh(current);
|
|
6973
|
+
this.#inFlightRefresh = run;
|
|
6974
|
+
try {
|
|
6975
|
+
return await run;
|
|
6976
|
+
} finally {
|
|
6977
|
+
this.#inFlightRefresh = null;
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
async #doRefresh(current) {
|
|
5580
6981
|
if (current.refreshToken) {
|
|
6982
|
+
let next = null;
|
|
5581
6983
|
try {
|
|
5582
|
-
|
|
6984
|
+
next = await this.#deps.refresh({
|
|
5583
6985
|
refreshToken: current.refreshToken,
|
|
5584
6986
|
region: this.#opts.region,
|
|
5585
6987
|
...this.#opts.country !== void 0 ? { country: this.#opts.country } : {},
|
|
5586
6988
|
...this.#opts.lang !== void 0 ? { lang: this.#opts.lang } : {},
|
|
5587
6989
|
...this.#opts.fetchImpl !== void 0 ? { fetchImpl: this.#opts.fetchImpl } : {}
|
|
5588
6990
|
});
|
|
6991
|
+
} catch {
|
|
6992
|
+
next = null;
|
|
6993
|
+
}
|
|
6994
|
+
if (next) {
|
|
5589
6995
|
await this.#adoptSession(next);
|
|
5590
6996
|
return next;
|
|
5591
|
-
} catch {
|
|
5592
6997
|
}
|
|
5593
6998
|
}
|
|
5594
6999
|
const fresh = await this.login();
|
|
@@ -5610,23 +7015,30 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5610
7015
|
(device) => this.#deps.createDevice({
|
|
5611
7016
|
device,
|
|
5612
7017
|
region: this.#opts.region,
|
|
5613
|
-
sessionRef: () => this.#requireSession()
|
|
7018
|
+
sessionRef: () => this.#requireSession(),
|
|
7019
|
+
onAuthFailure: () => this.reauthenticate()
|
|
5614
7020
|
})
|
|
5615
7021
|
);
|
|
5616
7022
|
for (const h of handles) {
|
|
5617
7023
|
h.on("stateChanged", (e) => this.emit("stateChanged", e));
|
|
5618
7024
|
h.on("event", (e) => this.emit("event", e));
|
|
5619
|
-
h.on("error", (err) =>
|
|
7025
|
+
h.on("error", (err) => {
|
|
7026
|
+
if (this.listenerCount("error") > 0) {
|
|
7027
|
+
this.emit("error", err);
|
|
7028
|
+
}
|
|
7029
|
+
});
|
|
5620
7030
|
await h.start();
|
|
5621
7031
|
}
|
|
5622
7032
|
const previous = this.#devices;
|
|
5623
7033
|
this.#devices = [...handles];
|
|
5624
7034
|
await Promise.all(previous.map((d) => d.close()));
|
|
7035
|
+
this.#scheduleProactiveRefresh();
|
|
5625
7036
|
return this.#devices;
|
|
5626
7037
|
}
|
|
5627
7038
|
/** Tear everything down: close every device push and clear timers. */
|
|
5628
7039
|
async close() {
|
|
5629
7040
|
this.#closed = true;
|
|
7041
|
+
this.#clearRefreshTimer();
|
|
5630
7042
|
const devices = this.#devices;
|
|
5631
7043
|
this.#devices = [];
|
|
5632
7044
|
await Promise.all(devices.map((d) => d.close()));
|
|
@@ -5641,13 +7053,68 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5641
7053
|
async #adoptSession(session) {
|
|
5642
7054
|
this.#session = session;
|
|
5643
7055
|
await this.#propagateSession(session);
|
|
7056
|
+
this.#scheduleProactiveRefresh();
|
|
7057
|
+
}
|
|
7058
|
+
#clearRefreshTimer() {
|
|
7059
|
+
if (this.#refreshTimer) {
|
|
7060
|
+
clearTimeout(this.#refreshTimer);
|
|
7061
|
+
this.#refreshTimer = null;
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
/** Arm the proactive-refresh timer `delayMs` from now (clamped to ≥0). */
|
|
7065
|
+
#armRefreshTimer(delayMs) {
|
|
7066
|
+
this.#clearRefreshTimer();
|
|
7067
|
+
if (this.#closed) {
|
|
7068
|
+
return;
|
|
7069
|
+
}
|
|
7070
|
+
this.#refreshTimer = setTimeout(
|
|
7071
|
+
() => {
|
|
7072
|
+
this.#refreshTimer = null;
|
|
7073
|
+
void this.#runProactiveRefresh();
|
|
7074
|
+
},
|
|
7075
|
+
Math.max(0, delayMs)
|
|
7076
|
+
);
|
|
7077
|
+
}
|
|
7078
|
+
/** (Re)arm the proactive-refresh timer against the current session's expiry. */
|
|
7079
|
+
#scheduleProactiveRefresh() {
|
|
7080
|
+
const session = this.#session;
|
|
7081
|
+
if (!session) {
|
|
7082
|
+
this.#clearRefreshTimer();
|
|
7083
|
+
return;
|
|
7084
|
+
}
|
|
7085
|
+
this.#armRefreshTimer(session.expiresAt - this.#leewayMs - Date.now());
|
|
7086
|
+
}
|
|
7087
|
+
/**
|
|
7088
|
+
* Timer body: refresh the session before it expires, then reschedule against
|
|
7089
|
+
* the new expiry. Because {@link ensureSession} propagates the refreshed token
|
|
7090
|
+
* to every device push, each MQTT connection is re-keyed BEFORE the broker
|
|
7091
|
+
* would reject the old one — the reactive re-auth loop never even starts.
|
|
7092
|
+
*/
|
|
7093
|
+
async #runProactiveRefresh() {
|
|
7094
|
+
if (this.#closed || this.#refreshInFlight) {
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
this.#refreshInFlight = true;
|
|
7098
|
+
try {
|
|
7099
|
+
await this.ensureSession();
|
|
7100
|
+
this.#refreshInFlight = false;
|
|
7101
|
+
this.#scheduleProactiveRefresh();
|
|
7102
|
+
} catch (err) {
|
|
7103
|
+
this.#refreshInFlight = false;
|
|
7104
|
+
if (this.listenerCount("error") > 0) {
|
|
7105
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
7106
|
+
}
|
|
7107
|
+
if (!this.#closed) {
|
|
7108
|
+
this.#armRefreshTimer(this.#leewayMs);
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
5644
7111
|
}
|
|
5645
7112
|
/** Push the refreshed token to every live device push. */
|
|
5646
7113
|
async #propagateSession(session) {
|
|
5647
7114
|
if (this.#closed) {
|
|
5648
7115
|
return;
|
|
5649
7116
|
}
|
|
5650
|
-
await Promise.
|
|
7117
|
+
await Promise.allSettled(this.#devices.map((d) => d.applySession(session)));
|
|
5651
7118
|
}
|
|
5652
7119
|
};
|
|
5653
7120
|
|
|
@@ -5764,59 +7231,59 @@ function redact(value) {
|
|
|
5764
7231
|
}
|
|
5765
7232
|
|
|
5766
7233
|
// src/diagnostics/dump-format.ts
|
|
5767
|
-
var
|
|
5768
|
-
var DumpScalarSchema =
|
|
5769
|
-
var PropertyObservationSchema =
|
|
5770
|
-
values:
|
|
5771
|
-
unmapped:
|
|
5772
|
-
enum:
|
|
5773
|
-
count:
|
|
5774
|
-
firstSeen:
|
|
5775
|
-
lastSeen:
|
|
7234
|
+
var import_zod6 = require("zod");
|
|
7235
|
+
var DumpScalarSchema = import_zod6.z.union([import_zod6.z.string(), import_zod6.z.number(), import_zod6.z.boolean()]);
|
|
7236
|
+
var PropertyObservationSchema = import_zod6.z.object({
|
|
7237
|
+
values: import_zod6.z.array(DumpScalarSchema),
|
|
7238
|
+
unmapped: import_zod6.z.array(DumpScalarSchema),
|
|
7239
|
+
enum: import_zod6.z.string().optional(),
|
|
7240
|
+
count: import_zod6.z.number(),
|
|
7241
|
+
firstSeen: import_zod6.z.number(),
|
|
7242
|
+
lastSeen: import_zod6.z.number()
|
|
5776
7243
|
});
|
|
5777
|
-
var EventObservationSchema =
|
|
5778
|
-
at:
|
|
5779
|
-
type:
|
|
5780
|
-
data:
|
|
7244
|
+
var EventObservationSchema = import_zod6.z.object({
|
|
7245
|
+
at: import_zod6.z.number(),
|
|
7246
|
+
type: import_zod6.z.string(),
|
|
7247
|
+
data: import_zod6.z.unknown().optional()
|
|
5781
7248
|
});
|
|
5782
|
-
var RawFrameSchema =
|
|
5783
|
-
at:
|
|
5784
|
-
source:
|
|
5785
|
-
payload:
|
|
7249
|
+
var RawFrameSchema = import_zod6.z.object({
|
|
7250
|
+
at: import_zod6.z.number(),
|
|
7251
|
+
source: import_zod6.z.string(),
|
|
7252
|
+
payload: import_zod6.z.unknown()
|
|
5786
7253
|
});
|
|
5787
|
-
var CommandSchema =
|
|
5788
|
-
name:
|
|
5789
|
-
siid:
|
|
5790
|
-
aiid:
|
|
7254
|
+
var CommandSchema = import_zod6.z.object({
|
|
7255
|
+
name: import_zod6.z.string(),
|
|
7256
|
+
siid: import_zod6.z.number().optional(),
|
|
7257
|
+
aiid: import_zod6.z.number().optional()
|
|
5791
7258
|
});
|
|
5792
|
-
var SensorSchema =
|
|
5793
|
-
model:
|
|
5794
|
-
channel:
|
|
7259
|
+
var SensorSchema = import_zod6.z.object({
|
|
7260
|
+
model: import_zod6.z.string(),
|
|
7261
|
+
channel: import_zod6.z.number().optional()
|
|
5795
7262
|
});
|
|
5796
|
-
var DeviceDumpSchema =
|
|
5797
|
-
schemaVersion:
|
|
5798
|
-
library:
|
|
5799
|
-
libraryVersion:
|
|
5800
|
-
device:
|
|
5801
|
-
model:
|
|
5802
|
-
firmware:
|
|
5803
|
-
region:
|
|
5804
|
-
type:
|
|
7263
|
+
var DeviceDumpSchema = import_zod6.z.object({
|
|
7264
|
+
schemaVersion: import_zod6.z.literal(1),
|
|
7265
|
+
library: import_zod6.z.union([import_zod6.z.literal("nodedreame"), import_zod6.z.literal("nodewitt")]),
|
|
7266
|
+
libraryVersion: import_zod6.z.string(),
|
|
7267
|
+
device: import_zod6.z.object({
|
|
7268
|
+
model: import_zod6.z.string(),
|
|
7269
|
+
firmware: import_zod6.z.string().optional(),
|
|
7270
|
+
region: import_zod6.z.string().optional(),
|
|
7271
|
+
type: import_zod6.z.string().optional()
|
|
5805
7272
|
}),
|
|
5806
|
-
observations:
|
|
5807
|
-
properties:
|
|
5808
|
-
events:
|
|
5809
|
-
rawFrames:
|
|
7273
|
+
observations: import_zod6.z.object({
|
|
7274
|
+
properties: import_zod6.z.record(import_zod6.z.string(), PropertyObservationSchema),
|
|
7275
|
+
events: import_zod6.z.array(EventObservationSchema),
|
|
7276
|
+
rawFrames: import_zod6.z.array(RawFrameSchema).optional()
|
|
5810
7277
|
}),
|
|
5811
|
-
catalog:
|
|
5812
|
-
commands:
|
|
5813
|
-
capabilities:
|
|
5814
|
-
sensors:
|
|
7278
|
+
catalog: import_zod6.z.object({
|
|
7279
|
+
commands: import_zod6.z.array(CommandSchema).optional(),
|
|
7280
|
+
capabilities: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()).optional(),
|
|
7281
|
+
sensors: import_zod6.z.array(SensorSchema).optional()
|
|
5815
7282
|
}),
|
|
5816
|
-
meta:
|
|
5817
|
-
startedAt:
|
|
5818
|
-
durationMs:
|
|
5819
|
-
generatedAt:
|
|
7283
|
+
meta: import_zod6.z.object({
|
|
7284
|
+
startedAt: import_zod6.z.number(),
|
|
7285
|
+
durationMs: import_zod6.z.number(),
|
|
7286
|
+
generatedAt: import_zod6.z.number()
|
|
5820
7287
|
})
|
|
5821
7288
|
});
|
|
5822
7289
|
|
|
@@ -6105,21 +7572,939 @@ function createDumper(target, options) {
|
|
|
6105
7572
|
function createClientDumper(client, options) {
|
|
6106
7573
|
return client.devices.map((d) => createDumper(d, options));
|
|
6107
7574
|
}
|
|
7575
|
+
|
|
7576
|
+
// src/video/monitor/detections.ts
|
|
7577
|
+
var asRecord = (value) => {
|
|
7578
|
+
if (typeof value === "string") {
|
|
7579
|
+
try {
|
|
7580
|
+
const parsed = JSON.parse(value);
|
|
7581
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
7582
|
+
} catch {
|
|
7583
|
+
return null;
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
return value && typeof value === "object" ? value : null;
|
|
7587
|
+
};
|
|
7588
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
7589
|
+
var toMs = (raw) => {
|
|
7590
|
+
const n = num(raw);
|
|
7591
|
+
if (n === void 0) return Date.now();
|
|
7592
|
+
return n > 1e15 ? Math.round(n / 1e6) : n;
|
|
7593
|
+
};
|
|
7594
|
+
var toBox = (raw) => {
|
|
7595
|
+
if (Array.isArray(raw) && raw.length >= 4) {
|
|
7596
|
+
const [x2, y2, w2, h2] = raw;
|
|
7597
|
+
if ([x2, y2, w2, h2].every((v) => num(v) !== void 0)) {
|
|
7598
|
+
return { x: x2, y: y2, w: w2, h: h2 };
|
|
7599
|
+
}
|
|
7600
|
+
return null;
|
|
7601
|
+
}
|
|
7602
|
+
const r = asRecord(raw);
|
|
7603
|
+
if (!r) return null;
|
|
7604
|
+
const x = num(r["x"]);
|
|
7605
|
+
const y = num(r["y"]);
|
|
7606
|
+
const w = num(r["w"]) ?? num(r["width"]);
|
|
7607
|
+
const h = num(r["h"]) ?? num(r["height"]);
|
|
7608
|
+
if (x === void 0 || y === void 0 || w === void 0 || h === void 0) return null;
|
|
7609
|
+
const label = typeof r["label"] === "string" ? r["label"] : void 0;
|
|
7610
|
+
const score = num(r["score"]) ?? num(r["confidence"]);
|
|
7611
|
+
return { x, y, w, h, ...label !== void 0 ? { label } : {}, ...score !== void 0 ? { score } : {} };
|
|
7612
|
+
};
|
|
7613
|
+
function parsePersonFollow(value) {
|
|
7614
|
+
const r = asRecord(value);
|
|
7615
|
+
if (!r) return null;
|
|
7616
|
+
return {
|
|
7617
|
+
timestampMs: toMs(r["timestamp"] ?? r["time"]),
|
|
7618
|
+
box: toBox(r["bbox"] ?? r["box"])
|
|
7619
|
+
};
|
|
7620
|
+
}
|
|
7621
|
+
function parseObstacleData(value) {
|
|
7622
|
+
const r = asRecord(value);
|
|
7623
|
+
if (!r) return null;
|
|
7624
|
+
const list = r["boxlist"] ?? r["boxes"] ?? r["boxList"];
|
|
7625
|
+
const boxes = [];
|
|
7626
|
+
if (Array.isArray(list)) {
|
|
7627
|
+
for (const item of list) {
|
|
7628
|
+
const box = toBox(item);
|
|
7629
|
+
if (box) boxes.push(box);
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
return { timestampMs: toMs(r["timestamp"] ?? r["time"]), boxes };
|
|
7633
|
+
}
|
|
7634
|
+
|
|
7635
|
+
// src/video/media/lv-rtmp-client.ts
|
|
7636
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
7637
|
+
var import_node_events2 = require("events");
|
|
7638
|
+
var import_node_crypto6 = require("crypto");
|
|
7639
|
+
|
|
7640
|
+
// src/video/media/amf0.ts
|
|
7641
|
+
var AMF0 = {
|
|
7642
|
+
NUMBER: 0,
|
|
7643
|
+
BOOLEAN: 1,
|
|
7644
|
+
STRING: 2,
|
|
7645
|
+
OBJECT: 3,
|
|
7646
|
+
NULL: 5,
|
|
7647
|
+
UNDEFINED: 6,
|
|
7648
|
+
ECMA_ARRAY: 8,
|
|
7649
|
+
OBJECT_END: 9,
|
|
7650
|
+
STRICT_ARRAY: 10,
|
|
7651
|
+
LONG_STRING: 12
|
|
7652
|
+
};
|
|
7653
|
+
function amfNumber(n) {
|
|
7654
|
+
const b = Buffer.alloc(9);
|
|
7655
|
+
b[0] = AMF0.NUMBER;
|
|
7656
|
+
b.writeDoubleBE(n, 1);
|
|
7657
|
+
return b;
|
|
7658
|
+
}
|
|
7659
|
+
function amfBoolean(v) {
|
|
7660
|
+
return Buffer.from([AMF0.BOOLEAN, v ? 1 : 0]);
|
|
7661
|
+
}
|
|
7662
|
+
function amfString(s) {
|
|
7663
|
+
const str2 = Buffer.from(s, "utf8");
|
|
7664
|
+
const b = Buffer.alloc(3 + str2.length);
|
|
7665
|
+
b[0] = AMF0.STRING;
|
|
7666
|
+
b.writeUInt16BE(str2.length, 1);
|
|
7667
|
+
str2.copy(b, 3);
|
|
7668
|
+
return b;
|
|
7669
|
+
}
|
|
7670
|
+
function amfNull() {
|
|
7671
|
+
return Buffer.from([AMF0.NULL]);
|
|
7672
|
+
}
|
|
7673
|
+
function amfNamed(key2, value) {
|
|
7674
|
+
const k = Buffer.from(key2, "utf8");
|
|
7675
|
+
const b = Buffer.alloc(2 + k.length);
|
|
7676
|
+
b.writeUInt16BE(k.length, 0);
|
|
7677
|
+
k.copy(b, 2);
|
|
7678
|
+
return Buffer.concat([b, value]);
|
|
7679
|
+
}
|
|
7680
|
+
function amfObject(pairs) {
|
|
7681
|
+
const parts = [Buffer.from([AMF0.OBJECT])];
|
|
7682
|
+
for (const [k, v] of pairs) parts.push(amfNamed(k, v));
|
|
7683
|
+
parts.push(Buffer.from([0, 0, AMF0.OBJECT_END]));
|
|
7684
|
+
return Buffer.concat(parts);
|
|
7685
|
+
}
|
|
7686
|
+
function decodeOne(buf, pos) {
|
|
7687
|
+
const type = buf[pos];
|
|
7688
|
+
let p = pos + 1;
|
|
7689
|
+
switch (type) {
|
|
7690
|
+
case AMF0.NUMBER:
|
|
7691
|
+
return { value: buf.readDoubleBE(p), next: p + 8 };
|
|
7692
|
+
case AMF0.BOOLEAN:
|
|
7693
|
+
return { value: buf[p] !== 0, next: p + 1 };
|
|
7694
|
+
case AMF0.STRING: {
|
|
7695
|
+
const len = buf.readUInt16BE(p);
|
|
7696
|
+
return { value: buf.toString("utf8", p + 2, p + 2 + len), next: p + 2 + len };
|
|
7697
|
+
}
|
|
7698
|
+
case AMF0.LONG_STRING: {
|
|
7699
|
+
const len = buf.readUInt32BE(p);
|
|
7700
|
+
return { value: buf.toString("utf8", p + 4, p + 4 + len), next: p + 4 + len };
|
|
7701
|
+
}
|
|
7702
|
+
case AMF0.NULL:
|
|
7703
|
+
return { value: null, next: p };
|
|
7704
|
+
case AMF0.UNDEFINED:
|
|
7705
|
+
return { value: void 0, next: p };
|
|
7706
|
+
case AMF0.ECMA_ARRAY:
|
|
7707
|
+
case AMF0.OBJECT: {
|
|
7708
|
+
if (type === AMF0.ECMA_ARRAY) p += 4;
|
|
7709
|
+
const obj = {};
|
|
7710
|
+
for (; ; ) {
|
|
7711
|
+
const klen = buf.readUInt16BE(p);
|
|
7712
|
+
if (klen === 0 && buf[p + 2] === AMF0.OBJECT_END) return { value: obj, next: p + 3 };
|
|
7713
|
+
const key2 = buf.toString("utf8", p + 2, p + 2 + klen);
|
|
7714
|
+
const r = decodeOne(buf, p + 2 + klen);
|
|
7715
|
+
obj[key2] = r.value;
|
|
7716
|
+
p = r.next;
|
|
7717
|
+
}
|
|
7718
|
+
}
|
|
7719
|
+
case AMF0.STRICT_ARRAY: {
|
|
7720
|
+
const count = buf.readUInt32BE(p);
|
|
7721
|
+
p += 4;
|
|
7722
|
+
const arr = [];
|
|
7723
|
+
for (let i = 0; i < count; i++) {
|
|
7724
|
+
const r = decodeOne(buf, p);
|
|
7725
|
+
arr.push(r.value);
|
|
7726
|
+
p = r.next;
|
|
7727
|
+
}
|
|
7728
|
+
return { value: arr, next: p };
|
|
7729
|
+
}
|
|
7730
|
+
default:
|
|
7731
|
+
throw new Error(`AMF0: unsupported type 0x${(type ?? -1).toString(16)} at ${pos}`);
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
function decodeAmf0(buf) {
|
|
7735
|
+
const out = [];
|
|
7736
|
+
let pos = 0;
|
|
7737
|
+
while (pos < buf.length) {
|
|
7738
|
+
const r = decodeOne(buf, pos);
|
|
7739
|
+
out.push(r.value);
|
|
7740
|
+
pos = r.next;
|
|
7741
|
+
}
|
|
7742
|
+
return out;
|
|
7743
|
+
}
|
|
7744
|
+
|
|
7745
|
+
// src/video/media/chunk.ts
|
|
7746
|
+
var MSG = {
|
|
7747
|
+
SET_CHUNK_SIZE: 1,
|
|
7748
|
+
ABORT: 2,
|
|
7749
|
+
ACK: 3,
|
|
7750
|
+
USER_CONTROL: 4,
|
|
7751
|
+
WINDOW_ACK_SIZE: 5,
|
|
7752
|
+
SET_PEER_BW: 6,
|
|
7753
|
+
AUDIO: 8,
|
|
7754
|
+
VIDEO: 9,
|
|
7755
|
+
DATA_AMF0: 18,
|
|
7756
|
+
COMMAND_AMF0: 20
|
|
7757
|
+
};
|
|
7758
|
+
var PRIVATE_MODE_CHUNK_SIZE = 6e4;
|
|
7759
|
+
function basicHeader(fmt, csid) {
|
|
7760
|
+
if (csid < 64) return Buffer.from([fmt << 6 | csid]);
|
|
7761
|
+
if (csid < 320) return Buffer.from([fmt << 6, csid - 64]);
|
|
7762
|
+
return Buffer.from([fmt << 6 | 1, csid - 64 & 255, csid - 64 >> 8]);
|
|
7763
|
+
}
|
|
7764
|
+
function encodeMessage(o) {
|
|
7765
|
+
if (o.body.length > PRIVATE_MODE_CHUNK_SIZE) {
|
|
7766
|
+
throw new Error(`message body ${o.body.length} exceeds private-mode chunk size ${PRIVATE_MODE_CHUNK_SIZE}`);
|
|
7767
|
+
}
|
|
7768
|
+
const ts = o.timestamp ?? 0;
|
|
7769
|
+
const hdrLen = o.fmt === 0 ? 11 : 7;
|
|
7770
|
+
const hdr = Buffer.alloc(hdrLen);
|
|
7771
|
+
hdr.writeUIntBE(ts >= 16777215 ? 16777215 : ts, 0, 3);
|
|
7772
|
+
hdr.writeUIntBE(o.body.length, 3, 3);
|
|
7773
|
+
hdr[6] = o.type;
|
|
7774
|
+
if (o.fmt === 0) hdr.writeUInt32LE(o.msid ?? 0, 7);
|
|
7775
|
+
const ext = ts >= 16777215 ? (() => {
|
|
7776
|
+
const e = Buffer.alloc(4);
|
|
7777
|
+
e.writeUInt32BE(ts, 0);
|
|
7778
|
+
return e;
|
|
7779
|
+
})() : Buffer.alloc(0);
|
|
7780
|
+
return Buffer.concat([basicHeader(o.fmt, o.csid), hdr, ext, o.body]);
|
|
7781
|
+
}
|
|
7782
|
+
function freshState() {
|
|
7783
|
+
return { timestamp: 0, delta: 0, length: 0, type: 0, msid: 0, extTs: false, partial: [], received: 0 };
|
|
7784
|
+
}
|
|
7785
|
+
var ChunkReader = class {
|
|
7786
|
+
chunkSize;
|
|
7787
|
+
pending = Buffer.alloc(0);
|
|
7788
|
+
streams = /* @__PURE__ */ new Map();
|
|
7789
|
+
constructor(chunkSize = PRIVATE_MODE_CHUNK_SIZE) {
|
|
7790
|
+
this.chunkSize = chunkSize;
|
|
7791
|
+
}
|
|
7792
|
+
feed(data) {
|
|
7793
|
+
this.pending = this.pending.length ? Buffer.concat([this.pending, data]) : data;
|
|
7794
|
+
const out = [];
|
|
7795
|
+
for (; ; ) {
|
|
7796
|
+
const consumed = this.tryReadChunk(out);
|
|
7797
|
+
if (consumed === 0) break;
|
|
7798
|
+
this.pending = this.pending.subarray(consumed);
|
|
7799
|
+
}
|
|
7800
|
+
return out;
|
|
7801
|
+
}
|
|
7802
|
+
/** Returns bytes consumed for one chunk, or 0 if more data is needed. */
|
|
7803
|
+
tryReadChunk(out) {
|
|
7804
|
+
const buf = this.pending;
|
|
7805
|
+
if (buf.length < 1) return 0;
|
|
7806
|
+
const fmt = buf[0] >> 6;
|
|
7807
|
+
let csid = buf[0] & 63;
|
|
7808
|
+
let p = 1;
|
|
7809
|
+
if (csid === 0) {
|
|
7810
|
+
if (buf.length < 2) return 0;
|
|
7811
|
+
csid = 64 + buf[1];
|
|
7812
|
+
p = 2;
|
|
7813
|
+
} else if (csid === 1) {
|
|
7814
|
+
if (buf.length < 3) return 0;
|
|
7815
|
+
csid = 64 + buf[1] + buf[2] * 256;
|
|
7816
|
+
p = 3;
|
|
7817
|
+
}
|
|
7818
|
+
const st = this.streams.get(csid) ?? freshState();
|
|
7819
|
+
const hdrLen = fmt === 0 ? 11 : fmt === 1 ? 7 : fmt === 2 ? 3 : 0;
|
|
7820
|
+
if (buf.length < p + hdrLen) return 0;
|
|
7821
|
+
let tsField = 0;
|
|
7822
|
+
if (fmt <= 2) {
|
|
7823
|
+
tsField = buf.readUIntBE(p, 3);
|
|
7824
|
+
if (fmt <= 1) {
|
|
7825
|
+
st.length = buf.readUIntBE(p + 3, 3);
|
|
7826
|
+
st.type = buf[p + 6];
|
|
7827
|
+
}
|
|
7828
|
+
if (fmt === 0) st.msid = buf.readUInt32LE(p + 7);
|
|
7829
|
+
st.extTs = tsField === 16777215;
|
|
7830
|
+
}
|
|
7831
|
+
p += hdrLen;
|
|
7832
|
+
if (st.extTs) {
|
|
7833
|
+
if (buf.length < p + 4) return 0;
|
|
7834
|
+
tsField = buf.readUInt32BE(p);
|
|
7835
|
+
p += 4;
|
|
7836
|
+
}
|
|
7837
|
+
const startOfMessage = st.received === 0;
|
|
7838
|
+
if (startOfMessage) {
|
|
7839
|
+
if (fmt === 0) {
|
|
7840
|
+
st.timestamp = tsField;
|
|
7841
|
+
st.delta = 0;
|
|
7842
|
+
} else if (fmt <= 2) {
|
|
7843
|
+
st.delta = tsField;
|
|
7844
|
+
st.timestamp += tsField;
|
|
7845
|
+
} else st.timestamp += st.delta;
|
|
7846
|
+
}
|
|
7847
|
+
const take = Math.min(this.chunkSize, st.length - st.received);
|
|
7848
|
+
if (buf.length < p + take) return 0;
|
|
7849
|
+
st.partial.push(Buffer.from(buf.subarray(p, p + take)));
|
|
7850
|
+
st.received += take;
|
|
7851
|
+
if (st.received >= st.length) {
|
|
7852
|
+
out.push({ csid, msid: st.msid, timestamp: st.timestamp, type: st.type, body: Buffer.concat(st.partial) });
|
|
7853
|
+
st.partial = [];
|
|
7854
|
+
st.received = 0;
|
|
7855
|
+
}
|
|
7856
|
+
this.streams.set(csid, st);
|
|
7857
|
+
return p + take;
|
|
7858
|
+
}
|
|
7859
|
+
};
|
|
7860
|
+
|
|
7861
|
+
// src/video/media/flv.ts
|
|
7862
|
+
var ANNEXB_START = Buffer.from([0, 0, 0, 1]);
|
|
7863
|
+
function parseVideoTag(body) {
|
|
7864
|
+
if (body.length < 5) return null;
|
|
7865
|
+
const b0 = body[0];
|
|
7866
|
+
return {
|
|
7867
|
+
frameType: b0 >> 4,
|
|
7868
|
+
codecId: b0 & 15,
|
|
7869
|
+
avcPacketType: body[1],
|
|
7870
|
+
compositionTime: body.readIntBE(2, 3),
|
|
7871
|
+
data: body.subarray(5)
|
|
7872
|
+
};
|
|
7873
|
+
}
|
|
7874
|
+
function parseAudioTag(body) {
|
|
7875
|
+
if (body.length < 1) return null;
|
|
7876
|
+
const b0 = body[0];
|
|
7877
|
+
const soundFormat = b0 >> 4;
|
|
7878
|
+
const isAac = soundFormat === 10;
|
|
7879
|
+
if (isAac && body.length < 2) return null;
|
|
7880
|
+
return {
|
|
7881
|
+
soundFormat,
|
|
7882
|
+
sampleRateIndex: b0 >> 2 & 3,
|
|
7883
|
+
channels: (b0 & 1) + 1,
|
|
7884
|
+
aacPacketType: isAac ? body[1] : null,
|
|
7885
|
+
data: body.subarray(isAac ? 2 : 1)
|
|
7886
|
+
};
|
|
7887
|
+
}
|
|
7888
|
+
function parseAvcConfig(rec) {
|
|
7889
|
+
const nalLengthSize = (rec[4] & 3) + 1;
|
|
7890
|
+
let p = 5;
|
|
7891
|
+
const sps = [];
|
|
7892
|
+
const pps = [];
|
|
7893
|
+
const numSps = rec[p++] & 31;
|
|
7894
|
+
for (let i = 0; i < numSps; i++) {
|
|
7895
|
+
const n = rec.readUInt16BE(p);
|
|
7896
|
+
sps.push(Buffer.from(rec.subarray(p + 2, p + 2 + n)));
|
|
7897
|
+
p += 2 + n;
|
|
7898
|
+
}
|
|
7899
|
+
const numPps = rec[p++];
|
|
7900
|
+
for (let i = 0; i < numPps; i++) {
|
|
7901
|
+
const n = rec.readUInt16BE(p);
|
|
7902
|
+
pps.push(Buffer.from(rec.subarray(p + 2, p + 2 + n)));
|
|
7903
|
+
p += 2 + n;
|
|
7904
|
+
}
|
|
7905
|
+
return { sps, pps, nalLengthSize };
|
|
7906
|
+
}
|
|
7907
|
+
function avccToAnnexB(data, cfg, prependParams) {
|
|
7908
|
+
const parts = [];
|
|
7909
|
+
if (prependParams) for (const nal of [...cfg.sps, ...cfg.pps]) parts.push(ANNEXB_START, nal);
|
|
7910
|
+
let p = 0;
|
|
7911
|
+
while (p + cfg.nalLengthSize <= data.length) {
|
|
7912
|
+
const len = data.readUIntBE(p, cfg.nalLengthSize);
|
|
7913
|
+
p += cfg.nalLengthSize;
|
|
7914
|
+
if (len === 0 || p + len > data.length) break;
|
|
7915
|
+
parts.push(ANNEXB_START, data.subarray(p, p + len));
|
|
7916
|
+
p += len;
|
|
7917
|
+
}
|
|
7918
|
+
return Buffer.concat(parts);
|
|
7919
|
+
}
|
|
7920
|
+
function parseAacConfig(asc) {
|
|
7921
|
+
const a0 = asc[0];
|
|
7922
|
+
const a1 = asc[1];
|
|
7923
|
+
return {
|
|
7924
|
+
objectType: a0 >> 3,
|
|
7925
|
+
samplingFrequencyIndex: (a0 & 7) << 1 | a1 >> 7,
|
|
7926
|
+
channelConfiguration: a1 >> 3 & 15
|
|
7927
|
+
};
|
|
7928
|
+
}
|
|
7929
|
+
var AAC_SAMPLE_RATES = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
|
|
7930
|
+
function aacSampleRate(cfg) {
|
|
7931
|
+
return AAC_SAMPLE_RATES[cfg.samplingFrequencyIndex] ?? 0;
|
|
7932
|
+
}
|
|
7933
|
+
function aacToAdts(raw, cfg) {
|
|
7934
|
+
const frameLen = raw.length + 7;
|
|
7935
|
+
const h = Buffer.alloc(7);
|
|
7936
|
+
h[0] = 255;
|
|
7937
|
+
h[1] = 241;
|
|
7938
|
+
h[2] = cfg.objectType - 1 << 6 | cfg.samplingFrequencyIndex << 2 | cfg.channelConfiguration >> 2;
|
|
7939
|
+
h[3] = (cfg.channelConfiguration & 3) << 6 | frameLen >> 11 & 3;
|
|
7940
|
+
h[4] = frameLen >> 3 & 255;
|
|
7941
|
+
h[5] = (frameLen & 7) << 5 | 31;
|
|
7942
|
+
h[6] = 252;
|
|
7943
|
+
return Buffer.concat([h, raw]);
|
|
7944
|
+
}
|
|
7945
|
+
|
|
7946
|
+
// src/video/media/lv-rtmp-client.ts
|
|
7947
|
+
var LV_STATUS = {
|
|
7948
|
+
REQUEST_AUDIO_TYPE: "NetStream.RequestAudioType",
|
|
7949
|
+
FORCE_IFRAME_START: "NetStream.ForceIFrame.Start",
|
|
7950
|
+
PING_START: "NetStream.Ping.Start",
|
|
7951
|
+
PING_RESPONSE: "NetStream.Ping.Response",
|
|
7952
|
+
PRE_CONNECT: "NetStream.PreConnect",
|
|
7953
|
+
KEEPALIVE: "NetStream.Keepalive",
|
|
7954
|
+
SERVER_INFO: "NetStream.ServerInfo",
|
|
7955
|
+
TRANSFER_DATA: "NetStream.TransferData",
|
|
7956
|
+
PLAY_START: "NetStream.Play.Start",
|
|
7957
|
+
/** Server signals the talk (intercom uplink) channel is ready. */
|
|
7958
|
+
TALK_READY: "NetStream.TalkReady"
|
|
7959
|
+
};
|
|
7960
|
+
var TALK_AUDIO_HEADER_G711A = 114;
|
|
7961
|
+
var CONNECT_CAPABILITIES = 15;
|
|
7962
|
+
var CONNECT_AUDIO_CODECS = 3191;
|
|
7963
|
+
var CONNECT_VIDEO_CODECS = 252;
|
|
7964
|
+
var CONNECT_VIDEO_FUNCTION = 1;
|
|
7965
|
+
var PLAY_START_LIVE = -1e3;
|
|
7966
|
+
var HANDSHAKE_C0_PRIVATE = 4;
|
|
7967
|
+
var HANDSHAKE_S0S1_LEN = 1537;
|
|
7968
|
+
var DEFAULT_PING_INTERVAL_MS = 3e4;
|
|
7969
|
+
var DEFAULT_CACHE_DURATION_MS = 3e3;
|
|
7970
|
+
var ACK_THRESHOLD_BYTES = 25e4;
|
|
7971
|
+
var CSID_CONTROL = 2;
|
|
7972
|
+
var CSID_COMMAND = 3;
|
|
7973
|
+
var CSID_TALK = 4;
|
|
7974
|
+
var CSID_PLAY = 8;
|
|
7975
|
+
var PLAY_STREAM_ID = 1;
|
|
7976
|
+
function parseRelayUrl(url, cacheDurationMs) {
|
|
7977
|
+
const m = /^rtmp:\/\/([^/:]+)(?::(\d+))?\/(.*)$/.exec(url);
|
|
7978
|
+
if (!m) throw new Error(`not an rtmp:// url: ${url}`);
|
|
7979
|
+
const host = m[1];
|
|
7980
|
+
const port = m[2] ? Number(m[2]) : 1935;
|
|
7981
|
+
const path = m[3];
|
|
7982
|
+
const slash = path.lastIndexOf("/");
|
|
7983
|
+
if (slash < 0) throw new Error(`relay url has no playpath: ${url}`);
|
|
7984
|
+
let app = path.slice(0, slash);
|
|
7985
|
+
const playpath = path.slice(slash + 1);
|
|
7986
|
+
if (!/[?&]cacheDuration=/.test(app)) app += `${app.includes("?") ? "&" : "?"}cacheDuration=${cacheDurationMs}`;
|
|
7987
|
+
return { host, port, app, playpath, tcUrl: `rtmp://${host}:${port}/${app}` };
|
|
7988
|
+
}
|
|
7989
|
+
var LvRtmpClient = class extends import_node_events2.EventEmitter {
|
|
7990
|
+
opts;
|
|
7991
|
+
endpoint;
|
|
7992
|
+
socket = null;
|
|
7993
|
+
handshakeBuf = Buffer.alloc(0);
|
|
7994
|
+
handshakeDone = false;
|
|
7995
|
+
c1Time = 0;
|
|
7996
|
+
reader = new ChunkReader(PRIVATE_MODE_CHUNK_SIZE);
|
|
7997
|
+
transactionId = 0;
|
|
7998
|
+
bytesIn = 0;
|
|
7999
|
+
bytesAcked = 0;
|
|
8000
|
+
ackWindow = ACK_THRESHOLD_BYTES;
|
|
8001
|
+
pingTimer = null;
|
|
8002
|
+
firstMessageSeen = false;
|
|
8003
|
+
avcConfig = null;
|
|
8004
|
+
aacConfig = null;
|
|
8005
|
+
closed = false;
|
|
8006
|
+
constructor(options) {
|
|
8007
|
+
super();
|
|
8008
|
+
this.opts = {
|
|
8009
|
+
url: options.url,
|
|
8010
|
+
cacheDurationMs: options.cacheDurationMs ?? DEFAULT_CACHE_DURATION_MS,
|
|
8011
|
+
requestAudioType: options.requestAudioType ?? true,
|
|
8012
|
+
ping: options.ping ?? true,
|
|
8013
|
+
pingIntervalMs: options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS,
|
|
8014
|
+
forceIFrameOnStart: options.forceIFrameOnStart ?? false,
|
|
8015
|
+
connectTimeoutMs: options.connectTimeoutMs ?? 1e4,
|
|
8016
|
+
log: options.log ?? (() => {
|
|
8017
|
+
})
|
|
8018
|
+
};
|
|
8019
|
+
this.endpoint = parseRelayUrl(this.opts.url, this.opts.cacheDurationMs);
|
|
8020
|
+
}
|
|
8021
|
+
/** Open TCP, write C0+C1 and the pipelined connect/play/RequestAudioType. Resolves once written. */
|
|
8022
|
+
connect() {
|
|
8023
|
+
return new Promise((resolve, reject) => {
|
|
8024
|
+
const sock = import_node_net.default.createConnection({ host: this.endpoint.host, port: this.endpoint.port });
|
|
8025
|
+
this.socket = sock;
|
|
8026
|
+
sock.setNoDelay(true);
|
|
8027
|
+
sock.setTimeout(this.opts.connectTimeoutMs, () => {
|
|
8028
|
+
if (!this.handshakeDone) this.fail(new Error("handshake timeout"));
|
|
8029
|
+
});
|
|
8030
|
+
sock.once("error", (err) => {
|
|
8031
|
+
this.emit("error", err);
|
|
8032
|
+
reject(err);
|
|
8033
|
+
});
|
|
8034
|
+
sock.on("data", (d) => this.onData(d));
|
|
8035
|
+
sock.on("close", () => this.onClose());
|
|
8036
|
+
sock.once("connect", () => {
|
|
8037
|
+
this.opts.log(`tcp connected ${this.endpoint.host}:${this.endpoint.port}`);
|
|
8038
|
+
sock.write(Buffer.concat([this.buildC0C1(), ...this.buildOpeningSequence()]));
|
|
8039
|
+
this.emit("connected");
|
|
8040
|
+
resolve();
|
|
8041
|
+
});
|
|
8042
|
+
});
|
|
8043
|
+
}
|
|
8044
|
+
close() {
|
|
8045
|
+
this.closed = true;
|
|
8046
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
8047
|
+
this.pingTimer = null;
|
|
8048
|
+
this.socket?.destroy();
|
|
8049
|
+
}
|
|
8050
|
+
/** onStatus encoder — byte-exact replica of liblvmedia IOT_RTMP_SendMessage (csid 3, fmt 1, ts 0). */
|
|
8051
|
+
buildStatus(code, extra) {
|
|
8052
|
+
const pairs = [
|
|
8053
|
+
["level", amfString("status")],
|
|
8054
|
+
["code", amfString(code)],
|
|
8055
|
+
["description", amfString("desc")]
|
|
8056
|
+
];
|
|
8057
|
+
if (extra) pairs.push([extra[0], amfString(extra[1])]);
|
|
8058
|
+
const body = Buffer.concat([amfString("onStatus"), amfNumber(++this.transactionId), amfNull(), amfObject(pairs)]);
|
|
8059
|
+
return encodeMessage({ fmt: 1, csid: CSID_COMMAND, type: MSG.COMMAND_AMF0, body });
|
|
8060
|
+
}
|
|
8061
|
+
sendStatus(code, extra) {
|
|
8062
|
+
this.write(this.buildStatus(code, extra));
|
|
8063
|
+
this.opts.log(`>> onStatus ${code}${extra ? ` ${extra[0]}=${extra[1]}` : ""} (txn ${this.transactionId})`);
|
|
8064
|
+
}
|
|
8065
|
+
forceIFrame() {
|
|
8066
|
+
this.sendStatus(LV_STATUS.FORCE_IFRAME_START);
|
|
8067
|
+
}
|
|
8068
|
+
// ---------------------------------------------------------------- outgoing
|
|
8069
|
+
buildC0C1() {
|
|
8070
|
+
this.c1Time = Math.floor(performance.now()) >>> 0;
|
|
8071
|
+
const c1 = Buffer.alloc(1536);
|
|
8072
|
+
c1.writeUInt32BE(this.c1Time, 0);
|
|
8073
|
+
c1.writeUInt32BE(0, 4);
|
|
8074
|
+
(0, import_node_crypto6.randomBytes)(1528).copy(c1, 8);
|
|
8075
|
+
return Buffer.concat([Buffer.from([HANDSHAKE_C0_PRIVATE]), c1]);
|
|
8076
|
+
}
|
|
8077
|
+
buildConnect() {
|
|
8078
|
+
const obj = amfObject([
|
|
8079
|
+
["app", amfString(this.endpoint.app)],
|
|
8080
|
+
["tcUrl", amfString(this.endpoint.tcUrl)],
|
|
8081
|
+
["fpad", amfBoolean(false)],
|
|
8082
|
+
["capabilities", amfNumber(CONNECT_CAPABILITIES)],
|
|
8083
|
+
["audioCodecs", amfNumber(CONNECT_AUDIO_CODECS)],
|
|
8084
|
+
["videoCodecs", amfNumber(CONNECT_VIDEO_CODECS)],
|
|
8085
|
+
["videoFunction", amfNumber(CONNECT_VIDEO_FUNCTION)]
|
|
8086
|
+
]);
|
|
8087
|
+
const body = Buffer.concat([amfString("connect"), amfNumber(++this.transactionId), obj]);
|
|
8088
|
+
return encodeMessage({ fmt: 0, csid: CSID_COMMAND, msid: 0, type: MSG.COMMAND_AMF0, body });
|
|
8089
|
+
}
|
|
8090
|
+
buildPlay() {
|
|
8091
|
+
const body = Buffer.concat([
|
|
8092
|
+
amfString("play"),
|
|
8093
|
+
amfNumber(++this.transactionId),
|
|
8094
|
+
amfNull(),
|
|
8095
|
+
amfString(this.endpoint.playpath),
|
|
8096
|
+
amfNumber(PLAY_START_LIVE)
|
|
8097
|
+
]);
|
|
8098
|
+
return encodeMessage({ fmt: 0, csid: CSID_PLAY, msid: PLAY_STREAM_ID, type: MSG.COMMAND_AMF0, body });
|
|
8099
|
+
}
|
|
8100
|
+
/** Exposed for the byte-exact self-test against the pcap. */
|
|
8101
|
+
buildOpeningSequence() {
|
|
8102
|
+
const seq = [this.buildConnect(), this.buildPlay()];
|
|
8103
|
+
if (this.opts.requestAudioType) seq.push(this.buildStatus(LV_STATUS.REQUEST_AUDIO_TYPE, ["audio_type", "1"]));
|
|
8104
|
+
return seq;
|
|
8105
|
+
}
|
|
8106
|
+
/**
|
|
8107
|
+
* Push one intercom (talk-back) audio frame UPSTREAM to the device — an RTMP
|
|
8108
|
+
* type-8 audio message on the talk chunk stream (csid 4, msid 1), exactly as
|
|
8109
|
+
* liblvmedia's `IOT_RTMP_SendAudio` does. `payload` is the raw codec audio
|
|
8110
|
+
* (default framing = G.711 A-law); `tsMs` is a running millisecond timestamp.
|
|
8111
|
+
* Only meaningful after the server sends {@link LV_STATUS.TALK_READY}.
|
|
8112
|
+
*/
|
|
8113
|
+
sendAudioFrame(payload, tsMs, header = TALK_AUDIO_HEADER_G711A) {
|
|
8114
|
+
const body = Buffer.concat([Buffer.from([header]), payload]);
|
|
8115
|
+
this.write(
|
|
8116
|
+
encodeMessage({ fmt: 0, csid: CSID_TALK, msid: PLAY_STREAM_ID, timestamp: tsMs >>> 0, type: MSG.AUDIO, body })
|
|
8117
|
+
);
|
|
8118
|
+
}
|
|
8119
|
+
write(buf) {
|
|
8120
|
+
if (!this.socket || this.socket.destroyed) return;
|
|
8121
|
+
this.socket.write(buf);
|
|
8122
|
+
}
|
|
8123
|
+
sendAck() {
|
|
8124
|
+
const b = Buffer.alloc(4);
|
|
8125
|
+
b.writeUInt32BE(this.bytesIn >>> 0, 0);
|
|
8126
|
+
this.write(encodeMessage({ fmt: 0, csid: CSID_CONTROL, msid: 0, type: MSG.ACK, body: b }));
|
|
8127
|
+
this.bytesAcked = this.bytesIn;
|
|
8128
|
+
}
|
|
8129
|
+
sendPingResponse(serverTs) {
|
|
8130
|
+
const body = Buffer.concat([Buffer.from([0, 7]), serverTs]);
|
|
8131
|
+
this.write(encodeMessage({ fmt: 0, csid: CSID_CONTROL, msid: 0, type: MSG.USER_CONTROL, body }));
|
|
8132
|
+
}
|
|
8133
|
+
startPinging() {
|
|
8134
|
+
if (!this.opts.ping || this.pingTimer) return;
|
|
8135
|
+
this.sendStatus(LV_STATUS.PING_START);
|
|
8136
|
+
this.pingTimer = setInterval(() => this.sendStatus(LV_STATUS.PING_START), this.opts.pingIntervalMs);
|
|
8137
|
+
}
|
|
8138
|
+
// ---------------------------------------------------------------- incoming
|
|
8139
|
+
onData(data) {
|
|
8140
|
+
this.bytesIn += data.length;
|
|
8141
|
+
let payload = data;
|
|
8142
|
+
if (!this.handshakeDone) {
|
|
8143
|
+
this.handshakeBuf = Buffer.concat([this.handshakeBuf, data]);
|
|
8144
|
+
if (this.handshakeBuf.length < HANDSHAKE_S0S1_LEN) return;
|
|
8145
|
+
this.onHandshake(this.handshakeBuf.subarray(0, HANDSHAKE_S0S1_LEN));
|
|
8146
|
+
payload = this.handshakeBuf.subarray(HANDSHAKE_S0S1_LEN);
|
|
8147
|
+
this.handshakeBuf = Buffer.alloc(0);
|
|
8148
|
+
this.handshakeDone = true;
|
|
8149
|
+
if (payload.length === 0) return;
|
|
8150
|
+
}
|
|
8151
|
+
let messages;
|
|
8152
|
+
try {
|
|
8153
|
+
messages = this.reader.feed(payload);
|
|
8154
|
+
} catch (err) {
|
|
8155
|
+
this.fail(err instanceof Error ? err : new Error(String(err)));
|
|
8156
|
+
return;
|
|
8157
|
+
}
|
|
8158
|
+
for (const m of messages) this.dispatch(m);
|
|
8159
|
+
if (this.bytesIn - this.bytesAcked >= this.ackWindow) this.sendAck();
|
|
8160
|
+
}
|
|
8161
|
+
onHandshake(s0s1) {
|
|
8162
|
+
const s0 = s0s1[0];
|
|
8163
|
+
const serverTime = s0s1.readUInt32BE(1);
|
|
8164
|
+
const zero = s0s1.readUInt32BE(5);
|
|
8165
|
+
this.opts.log(`<< S0=0x${s0.toString(16)} S1.time=${serverTime} S1.zero=0x${zero.toString(16)} (client sent 0x04; app logs a mismatch but continues)`);
|
|
8166
|
+
}
|
|
8167
|
+
dispatch(m) {
|
|
8168
|
+
if (!this.firstMessageSeen) {
|
|
8169
|
+
this.firstMessageSeen = true;
|
|
8170
|
+
this.startPinging();
|
|
8171
|
+
}
|
|
8172
|
+
switch (m.type) {
|
|
8173
|
+
case MSG.SET_CHUNK_SIZE: {
|
|
8174
|
+
const size = m.body.readUInt32BE(0) & 2147483647;
|
|
8175
|
+
this.opts.log(`<< SetChunkSize ${size}`);
|
|
8176
|
+
this.reader.chunkSize = size;
|
|
8177
|
+
return;
|
|
8178
|
+
}
|
|
8179
|
+
case MSG.WINDOW_ACK_SIZE:
|
|
8180
|
+
this.ackWindow = Math.min(ACK_THRESHOLD_BYTES, m.body.readUInt32BE(0));
|
|
8181
|
+
this.opts.log(`<< WindowAckSize ${m.body.readUInt32BE(0)}`);
|
|
8182
|
+
return;
|
|
8183
|
+
case MSG.SET_PEER_BW:
|
|
8184
|
+
this.opts.log(`<< SetPeerBandwidth ${m.body.readUInt32BE(0)} limit=${m.body[4]}`);
|
|
8185
|
+
return;
|
|
8186
|
+
case MSG.ACK:
|
|
8187
|
+
case MSG.ABORT:
|
|
8188
|
+
return;
|
|
8189
|
+
case MSG.USER_CONTROL:
|
|
8190
|
+
return this.onUserControl(m.body);
|
|
8191
|
+
case MSG.COMMAND_AMF0:
|
|
8192
|
+
return this.onCommand(m);
|
|
8193
|
+
case MSG.DATA_AMF0:
|
|
8194
|
+
return this.onDataMessage(m.body);
|
|
8195
|
+
case MSG.AUDIO:
|
|
8196
|
+
return this.onAudio(m);
|
|
8197
|
+
case MSG.VIDEO:
|
|
8198
|
+
return this.onVideo(m);
|
|
8199
|
+
default:
|
|
8200
|
+
this.opts.log(`<< unknown message type ${m.type} len=${m.body.length}`);
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
onUserControl(body) {
|
|
8204
|
+
const event = body.readUInt16BE(0);
|
|
8205
|
+
const names = { 0: "StreamBegin", 1: "StreamEOF", 2: "StreamDry", 4: "StreamIsRecorded", 6: "PingRequest", 7: "PingResponse" };
|
|
8206
|
+
this.opts.log(`<< UserControl ${names[event] ?? event} ${body.subarray(2).toString("hex")}`);
|
|
8207
|
+
if (event === 6) this.sendPingResponse(body.subarray(2, 6));
|
|
8208
|
+
}
|
|
8209
|
+
onCommand(m) {
|
|
8210
|
+
let values;
|
|
8211
|
+
try {
|
|
8212
|
+
values = decodeAmf0(m.body);
|
|
8213
|
+
} catch (err) {
|
|
8214
|
+
this.opts.log(`<< undecodable command: ${m.body.toString("hex")}`);
|
|
8215
|
+
return;
|
|
8216
|
+
}
|
|
8217
|
+
const [name, txn, , info] = values;
|
|
8218
|
+
this.opts.log(`<< ${String(name)} ${JSON.stringify(values.slice(1))}`);
|
|
8219
|
+
this.emit("command", String(name), values);
|
|
8220
|
+
if (name === "onStatus" && info && typeof info === "object" && !Array.isArray(info)) {
|
|
8221
|
+
const fields = info;
|
|
8222
|
+
this.emit("status", {
|
|
8223
|
+
code: String(fields.code ?? ""),
|
|
8224
|
+
level: String(fields.level ?? ""),
|
|
8225
|
+
description: String(fields.description ?? ""),
|
|
8226
|
+
fields,
|
|
8227
|
+
transactionId: typeof txn === "number" ? txn : 0
|
|
8228
|
+
});
|
|
8229
|
+
if (fields.code === LV_STATUS.PLAY_START && this.opts.forceIFrameOnStart) this.forceIFrame();
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
onDataMessage(body) {
|
|
8233
|
+
try {
|
|
8234
|
+
const values = decodeAmf0(body);
|
|
8235
|
+
this.opts.log(`<< data ${JSON.stringify(values).slice(0, 300)}`);
|
|
8236
|
+
const obj = values.find((v) => v && typeof v === "object" && !Array.isArray(v));
|
|
8237
|
+
if (obj) this.emit("metadata", obj);
|
|
8238
|
+
} catch {
|
|
8239
|
+
this.opts.log(`<< undecodable data message len=${body.length}`);
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
onAudio(m) {
|
|
8243
|
+
this.emit("audio", { timestamp: m.timestamp, body: m.body });
|
|
8244
|
+
const tag = parseAudioTag(m.body);
|
|
8245
|
+
if (!tag) return;
|
|
8246
|
+
if (tag.soundFormat === 10) {
|
|
8247
|
+
if (tag.aacPacketType === 0) {
|
|
8248
|
+
this.aacConfig = parseAacConfig(tag.data);
|
|
8249
|
+
this.emit("audioInfo", { codec: "aac", sampleRate: aacSampleRate(this.aacConfig), channels: this.aacConfig.channelConfiguration });
|
|
8250
|
+
return;
|
|
8251
|
+
}
|
|
8252
|
+
if (this.aacConfig) this.emit("audioFrame", aacToAdts(tag.data, this.aacConfig));
|
|
8253
|
+
return;
|
|
8254
|
+
}
|
|
8255
|
+
if (tag.soundFormat === 7 || tag.soundFormat === 8) {
|
|
8256
|
+
if (!this.aacConfig) this.emit("audioInfo", { codec: tag.soundFormat === 7 ? "g711a" : "g711u", sampleRate: 8e3, channels: 1 });
|
|
8257
|
+
this.emit("audioFrame", tag.data);
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
8260
|
+
this.emit("audioInfo", { codec: "unknown", sampleRate: 0, channels: tag.channels });
|
|
8261
|
+
}
|
|
8262
|
+
onVideo(m) {
|
|
8263
|
+
this.emit("video", { timestamp: m.timestamp, body: m.body });
|
|
8264
|
+
const tag = parseVideoTag(m.body);
|
|
8265
|
+
if (!tag || tag.codecId !== 7) return;
|
|
8266
|
+
if (tag.avcPacketType === 0) {
|
|
8267
|
+
this.avcConfig = parseAvcConfig(tag.data);
|
|
8268
|
+
return;
|
|
8269
|
+
}
|
|
8270
|
+
if (tag.avcPacketType !== 1 || !this.avcConfig) return;
|
|
8271
|
+
const isKeyframe = tag.frameType === 1;
|
|
8272
|
+
this.emit("videoAccessUnit", {
|
|
8273
|
+
data: avccToAnnexB(tag.data, this.avcConfig, isKeyframe),
|
|
8274
|
+
isKeyframe,
|
|
8275
|
+
videoType: "H264",
|
|
8276
|
+
microseconds: m.timestamp * 1e3,
|
|
8277
|
+
time: m.timestamp + tag.compositionTime
|
|
8278
|
+
});
|
|
8279
|
+
}
|
|
8280
|
+
fail(err) {
|
|
8281
|
+
this.opts.log(`!! ${err.message}`);
|
|
8282
|
+
this.emit("error", err);
|
|
8283
|
+
this.close();
|
|
8284
|
+
}
|
|
8285
|
+
onClose() {
|
|
8286
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
8287
|
+
this.pingTimer = null;
|
|
8288
|
+
if (!this.closed) this.opts.log("socket closed by peer");
|
|
8289
|
+
this.closed = true;
|
|
8290
|
+
this.emit("close");
|
|
8291
|
+
}
|
|
8292
|
+
};
|
|
8293
|
+
|
|
8294
|
+
// src/video/media/g711.ts
|
|
8295
|
+
var ALAW_SEG_END = [31, 63, 127, 255, 511, 1023, 2047, 4095];
|
|
8296
|
+
function pcm16ToALaw(sample) {
|
|
8297
|
+
let s = sample;
|
|
8298
|
+
let sign = ~s >> 8 & 128;
|
|
8299
|
+
if (sign === 0) s = -s;
|
|
8300
|
+
if (s > 32635) s = 32635;
|
|
8301
|
+
let aval;
|
|
8302
|
+
if (s >= 256) {
|
|
8303
|
+
let seg = 7;
|
|
8304
|
+
for (let i = 0; i < 8; i++) {
|
|
8305
|
+
if (s <= ALAW_SEG_END[i]) {
|
|
8306
|
+
seg = i;
|
|
8307
|
+
break;
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
const mantissa = s >> seg + 3 & 15;
|
|
8311
|
+
aval = seg << 4 | mantissa;
|
|
8312
|
+
} else {
|
|
8313
|
+
aval = s >> 4;
|
|
8314
|
+
}
|
|
8315
|
+
return (aval ^ sign ^ 85) & 255;
|
|
8316
|
+
}
|
|
8317
|
+
function pcm16leToALaw(pcm) {
|
|
8318
|
+
const count = pcm.length >> 1;
|
|
8319
|
+
const out = Buffer.allocUnsafe(count);
|
|
8320
|
+
for (let i = 0; i < count; i++) {
|
|
8321
|
+
out[i] = pcm16ToALaw(pcm.readInt16LE(i * 2));
|
|
8322
|
+
}
|
|
8323
|
+
return out;
|
|
8324
|
+
}
|
|
8325
|
+
|
|
8326
|
+
// src/video/media/camera-stream.ts
|
|
8327
|
+
var DreameCameraStream = class extends TypedEmitter {
|
|
8328
|
+
#controller;
|
|
8329
|
+
#clientFactory;
|
|
8330
|
+
#log;
|
|
8331
|
+
#client = null;
|
|
8332
|
+
#state = "idle";
|
|
8333
|
+
#videoFrames = 0;
|
|
8334
|
+
#audioFrames = 0;
|
|
8335
|
+
#talkReady = false;
|
|
8336
|
+
#talkWaiters = [];
|
|
8337
|
+
#talkTs = 0;
|
|
8338
|
+
constructor(input) {
|
|
8339
|
+
super();
|
|
8340
|
+
this.#controller = input.controller;
|
|
8341
|
+
this.#log = input.log ?? (() => {
|
|
8342
|
+
});
|
|
8343
|
+
this.#clientFactory = input.clientFactory ?? ((url) => new LvRtmpClient({
|
|
8344
|
+
url,
|
|
8345
|
+
...input.requestAudioType !== void 0 ? { requestAudioType: input.requestAudioType } : {},
|
|
8346
|
+
...input.ping !== void 0 ? { ping: input.ping } : {},
|
|
8347
|
+
...input.log ? { log: input.log } : {}
|
|
8348
|
+
}));
|
|
8349
|
+
}
|
|
8350
|
+
/** True while frames are flowing. */
|
|
8351
|
+
get isRunning() {
|
|
8352
|
+
return this.#state === "running";
|
|
8353
|
+
}
|
|
8354
|
+
/**
|
|
8355
|
+
* Cold-start the monitor, connect to the relay, and begin emitting frames.
|
|
8356
|
+
* Resolves once connected. Throws (and cleans up) if the monitor or relay
|
|
8357
|
+
* connection fails.
|
|
8358
|
+
*/
|
|
8359
|
+
async start() {
|
|
8360
|
+
if (this.#state !== "idle") {
|
|
8361
|
+
throw new DreameError(`camera stream cannot start from state '${this.#state}'`);
|
|
8362
|
+
}
|
|
8363
|
+
this.#state = "starting";
|
|
8364
|
+
try {
|
|
8365
|
+
this.#log("[nodedreame:stream] starting");
|
|
8366
|
+
const handle = await this.#controller.open();
|
|
8367
|
+
const client = this.#clientFactory(handle.rtmpUrl);
|
|
8368
|
+
this.#client = client;
|
|
8369
|
+
client.on("videoAccessUnit", (e) => {
|
|
8370
|
+
this.#videoFrames += 1;
|
|
8371
|
+
if (this.#videoFrames === 1) this.#log("[nodedreame:stream] first video access unit");
|
|
8372
|
+
this.emit("videoAccessUnit", e);
|
|
8373
|
+
});
|
|
8374
|
+
client.on("audioFrame", (buf) => {
|
|
8375
|
+
this.#audioFrames += 1;
|
|
8376
|
+
if (this.#audioFrames === 1) this.#log("[nodedreame:stream] first audio frame");
|
|
8377
|
+
this.emit("audioFrame", buf);
|
|
8378
|
+
});
|
|
8379
|
+
client.on("audioInfo", (info) => {
|
|
8380
|
+
this.#log(`[nodedreame:stream] audio ${info.codec} ${info.sampleRate}Hz x${info.channels}`);
|
|
8381
|
+
this.emit("audioInfo", info);
|
|
8382
|
+
});
|
|
8383
|
+
client.on("status", (e) => {
|
|
8384
|
+
if (e.code === LV_STATUS.TALK_READY) {
|
|
8385
|
+
this.#log("[nodedreame:stream] talk channel ready");
|
|
8386
|
+
this.#talkReady = true;
|
|
8387
|
+
const waiters = this.#talkWaiters;
|
|
8388
|
+
this.#talkWaiters = [];
|
|
8389
|
+
for (const w of waiters) w();
|
|
8390
|
+
}
|
|
8391
|
+
});
|
|
8392
|
+
client.on("error", (err) => {
|
|
8393
|
+
this.#log(`[nodedreame:stream] rtmp error: ${err.message}`);
|
|
8394
|
+
this.emit("error", err);
|
|
8395
|
+
});
|
|
8396
|
+
client.on("close", () => {
|
|
8397
|
+
this.#log("[nodedreame:stream] upstream relay closed");
|
|
8398
|
+
if (this.#state === "running") void this.stop();
|
|
8399
|
+
});
|
|
8400
|
+
await client.connect();
|
|
8401
|
+
this.#state = "running";
|
|
8402
|
+
this.#log("[nodedreame:stream] connected to relay");
|
|
8403
|
+
this.emit("started", { rtmpUrl: handle.rtmpUrl });
|
|
8404
|
+
return { rtmpUrl: handle.rtmpUrl };
|
|
8405
|
+
} catch (err) {
|
|
8406
|
+
this.#state = "idle";
|
|
8407
|
+
await this.#safeTeardown();
|
|
8408
|
+
throw err;
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
/** Close the relay connection and release the monitor. Idempotent. */
|
|
8412
|
+
async stop() {
|
|
8413
|
+
if (this.#state === "idle" || this.#state === "stopping") return;
|
|
8414
|
+
this.#state = "stopping";
|
|
8415
|
+
this.#log(`[nodedreame:stream] stopping (video=${this.#videoFrames} audio=${this.#audioFrames})`);
|
|
8416
|
+
await this.#safeTeardown();
|
|
8417
|
+
this.#state = "idle";
|
|
8418
|
+
this.emit("stopped");
|
|
8419
|
+
}
|
|
8420
|
+
/**
|
|
8421
|
+
* Open the two-way intercom: send the MIoT start, then resolve once the relay
|
|
8422
|
+
* signals the talk channel is ready (so it's safe to {@link sendTalkPcm}).
|
|
8423
|
+
* Rejects if not ready within `timeoutMs`.
|
|
8424
|
+
*/
|
|
8425
|
+
async startTalk(opts) {
|
|
8426
|
+
if (this.#state !== "running") throw new DreameError("stream is not running; start() first");
|
|
8427
|
+
this.#log("[nodedreame:stream] intercom start");
|
|
8428
|
+
await this.#controller.startIntercom(
|
|
8429
|
+
opts?.needRecordSound !== void 0 || opts?.videoCall !== void 0 ? { ...opts.needRecordSound !== void 0 ? { needRecordSound: opts.needRecordSound } : {}, ...opts.videoCall ? { videoCall: true } : {} } : void 0
|
|
8430
|
+
);
|
|
8431
|
+
this.#talkTs = 0;
|
|
8432
|
+
await this.whenTalkReady(opts?.timeoutMs ?? 5e3);
|
|
8433
|
+
}
|
|
8434
|
+
/** Resolve when the talk channel is ready (or reject on timeout). */
|
|
8435
|
+
whenTalkReady(timeoutMs = 5e3) {
|
|
8436
|
+
if (this.#talkReady) return Promise.resolve();
|
|
8437
|
+
return new Promise((resolve, reject) => {
|
|
8438
|
+
const timer = setTimeout(() => reject(new DreameError("timed out waiting for TalkReady")), timeoutMs);
|
|
8439
|
+
timer.unref?.();
|
|
8440
|
+
this.#talkWaiters.push(() => {
|
|
8441
|
+
clearTimeout(timer);
|
|
8442
|
+
resolve();
|
|
8443
|
+
});
|
|
8444
|
+
});
|
|
8445
|
+
}
|
|
8446
|
+
/**
|
|
8447
|
+
* Encode 16-bit LE mono PCM (8 kHz) to G.711 A-law and push it upstream.
|
|
8448
|
+
* `tsMs` defaults to a running clock advanced by the frame duration.
|
|
8449
|
+
*/
|
|
8450
|
+
sendTalkPcm(pcm16le, tsMs) {
|
|
8451
|
+
const alaw = pcm16leToALaw(pcm16le);
|
|
8452
|
+
this.sendTalkAlaw(alaw, tsMs);
|
|
8453
|
+
}
|
|
8454
|
+
/** Push raw A-law audio upstream (advancing the running talk clock). */
|
|
8455
|
+
sendTalkAlaw(alaw, tsMs) {
|
|
8456
|
+
if (!this.#client) throw new DreameError("stream is not running");
|
|
8457
|
+
const ts = tsMs ?? this.#talkTs;
|
|
8458
|
+
this.#client.sendAudioFrame(alaw, ts);
|
|
8459
|
+
this.#talkTs = ts + Math.round(alaw.length / 8);
|
|
8460
|
+
}
|
|
8461
|
+
/** Close the two-way intercom (MIoT stop). */
|
|
8462
|
+
async stopTalk() {
|
|
8463
|
+
this.#talkReady = false;
|
|
8464
|
+
this.#log("[nodedreame:stream] intercom stop");
|
|
8465
|
+
await this.#controller.stopIntercom();
|
|
8466
|
+
}
|
|
8467
|
+
async #safeTeardown() {
|
|
8468
|
+
this.#talkReady = false;
|
|
8469
|
+
this.#talkWaiters = [];
|
|
8470
|
+
try {
|
|
8471
|
+
this.#client?.close();
|
|
8472
|
+
} catch {
|
|
8473
|
+
}
|
|
8474
|
+
this.#client = null;
|
|
8475
|
+
try {
|
|
8476
|
+
await this.#controller.close();
|
|
8477
|
+
} catch (err) {
|
|
8478
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
};
|
|
6108
8482
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6109
8483
|
0 && (module.exports = {
|
|
6110
8484
|
AI_FEATURE_BIT,
|
|
6111
8485
|
AI_FEATURE_JSON_KEY,
|
|
8486
|
+
ALIYUN_REGION_ID,
|
|
6112
8487
|
AUTO_SWITCH_JSON_KEY,
|
|
6113
8488
|
BaseDevice,
|
|
6114
8489
|
ChargingStatus,
|
|
6115
8490
|
CleaningMode,
|
|
8491
|
+
DRIVE_DIRECTIONS,
|
|
6116
8492
|
DefaultCapabilityResolver,
|
|
6117
8493
|
DreameApiError,
|
|
6118
8494
|
DreameAuthError,
|
|
8495
|
+
DreameCameraController,
|
|
8496
|
+
DreameCameraStream,
|
|
6119
8497
|
DreameDeviceOfflineError,
|
|
6120
8498
|
DreameError,
|
|
6121
8499
|
DreameTransportError,
|
|
8500
|
+
DreameVideoSession,
|
|
6122
8501
|
LIBRARY_NAME,
|
|
8502
|
+
LV_STATUS,
|
|
8503
|
+
LvRtmpClient,
|
|
8504
|
+
MONITOR_AIID,
|
|
8505
|
+
MONITOR_PIID,
|
|
8506
|
+
MONITOR_SIID,
|
|
8507
|
+
MONITOR_VENDOR_TOKEN,
|
|
6123
8508
|
MOWER_MODEL_CAPABILITIES,
|
|
6124
8509
|
MOWER_TASK_SUBSTATES,
|
|
6125
8510
|
MiotError,
|
|
@@ -6132,33 +8517,65 @@ function createClientDumper(client, options) {
|
|
|
6132
8517
|
MowerStatus,
|
|
6133
8518
|
MowerTaskStatus,
|
|
6134
8519
|
Nodreame,
|
|
8520
|
+
PET_SOUNDS,
|
|
6135
8521
|
SuctionLevel,
|
|
8522
|
+
TALK_AUDIO_HEADER_G711A,
|
|
6136
8523
|
TaskStatus,
|
|
8524
|
+
VACUUM_ACTIONS,
|
|
6137
8525
|
VACUUM_CONSUMABLES,
|
|
6138
8526
|
VACUUM_MODEL_CAPABILITIES,
|
|
8527
|
+
VACUUM_MOVE,
|
|
8528
|
+
VACUUM_SIID,
|
|
6139
8529
|
VacuumCapabilityResolver,
|
|
6140
8530
|
VacuumDevice,
|
|
6141
8531
|
WaterVolume,
|
|
8532
|
+
aacSampleRate,
|
|
8533
|
+
aacToAdts,
|
|
8534
|
+
avccToAnnexB,
|
|
6142
8535
|
consumableSpec,
|
|
6143
8536
|
createClientDumper,
|
|
6144
8537
|
createDumper,
|
|
8538
|
+
createIotSession,
|
|
6145
8539
|
decodeAiFeature,
|
|
6146
8540
|
decodeAutoSwitch,
|
|
6147
8541
|
decodeAutoSwitchAll,
|
|
8542
|
+
decodeWifiSignalMap,
|
|
6148
8543
|
encodeAiFeatureWrite,
|
|
6149
8544
|
encodeAutoSwitchWrite,
|
|
6150
8545
|
extractMowerConsumableValues,
|
|
8546
|
+
getAliyunAuthCode,
|
|
8547
|
+
getDeviceVideoProfile,
|
|
6151
8548
|
getMowerCapabilities,
|
|
6152
8549
|
getVacuumCapabilities,
|
|
8550
|
+
getVideoAccessToken,
|
|
8551
|
+
getVideoFamilyId,
|
|
8552
|
+
hashAccessCode,
|
|
8553
|
+
isAuthRefusedError,
|
|
6153
8554
|
isDreameConsumableKey,
|
|
8555
|
+
listDevices,
|
|
8556
|
+
loginByOauth,
|
|
8557
|
+
makeMonitorSession,
|
|
6154
8558
|
mowerConsumableIndex,
|
|
6155
8559
|
mowerFaultSeverity,
|
|
8560
|
+
parseAacConfig,
|
|
8561
|
+
parseAvcConfig,
|
|
8562
|
+
parseConnectivity,
|
|
6156
8563
|
parseMowerConsumables,
|
|
6157
8564
|
parseMowerHeartbeat,
|
|
6158
8565
|
parseMowingProgress,
|
|
8566
|
+
parseObstacleData,
|
|
8567
|
+
parsePersonFollow,
|
|
8568
|
+
parseRelayUrl,
|
|
8569
|
+
pcm16ToALaw,
|
|
8570
|
+
pcm16leToALaw,
|
|
8571
|
+
remoteDriveValue,
|
|
6159
8572
|
renderMowerSvg,
|
|
6160
8573
|
renderVacuumPng,
|
|
8574
|
+
renderWifiSignalPng,
|
|
6161
8575
|
resolveCapabilities,
|
|
6162
|
-
|
|
8576
|
+
signApiGatewayRequest,
|
|
8577
|
+
streamQuery,
|
|
8578
|
+
supportedAutoSwitchKeys,
|
|
8579
|
+
toVideoProfile
|
|
6163
8580
|
});
|
|
6164
8581
|
//# sourceMappingURL=index.cjs.map
|