@apocaliss92/nodedreame 1.11.11 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,41 +66,73 @@ __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,
94
+ explainNoCameraChannel: () => explainNoCameraChannel,
73
95
  extractMowerConsumableValues: () => extractMowerConsumableValues,
96
+ getAliyunAuthCode: () => getAliyunAuthCode,
97
+ getDeviceVideoProfile: () => getDeviceVideoProfile,
74
98
  getMowerCapabilities: () => getMowerCapabilities,
75
99
  getVacuumCapabilities: () => getVacuumCapabilities,
100
+ getVideoAccessToken: () => getVideoAccessToken,
101
+ getVideoFamilyId: () => getVideoFamilyId,
102
+ hashAccessCode: () => hashAccessCode,
76
103
  isAuthRefusedError: () => isAuthRefusedError,
77
104
  isDreameConsumableKey: () => isDreameConsumableKey,
105
+ listDevices: () => listDevices,
106
+ loginByOauth: () => loginByOauth,
107
+ makeMonitorSession: () => makeMonitorSession,
78
108
  mowerConsumableIndex: () => mowerConsumableIndex,
79
109
  mowerFaultSeverity: () => mowerFaultSeverity,
110
+ parseAacConfig: () => parseAacConfig,
111
+ parseAvcConfig: () => parseAvcConfig,
112
+ parseConnectivity: () => parseConnectivity,
80
113
  parseMowerConsumables: () => parseMowerConsumables,
81
114
  parseMowerHeartbeat: () => parseMowerHeartbeat,
82
115
  parseMowingProgress: () => parseMowingProgress,
116
+ parseObstacleData: () => parseObstacleData,
117
+ parsePersonFollow: () => parsePersonFollow,
118
+ parseRelayUrl: () => parseRelayUrl,
119
+ pcm16ToALaw: () => pcm16ToALaw,
120
+ pcm16leToALaw: () => pcm16leToALaw,
121
+ remoteDriveValue: () => remoteDriveValue,
83
122
  renderMowerSvg: () => renderMowerSvg,
84
123
  renderVacuumPng: () => renderVacuumPng,
124
+ renderWifiSignalPng: () => renderWifiSignalPng,
85
125
  resolveCapabilities: () => resolveCapabilities,
86
- supportedAutoSwitchKeys: () => supportedAutoSwitchKeys
126
+ signApiGatewayRequest: () => signApiGatewayRequest,
127
+ streamQuery: () => streamQuery,
128
+ supportedAutoSwitchKeys: () => supportedAutoSwitchKeys,
129
+ toVideoProfile: () => toVideoProfile
87
130
  });
88
131
  module.exports = __toCommonJS(index_exports);
89
132
 
90
133
  // src/support/version.ts
91
134
  var LIBRARY_NAME = "nodedreame";
92
- var LIBRARY_VERSION = "1.11.10";
135
+ var LIBRARY_VERSION = "1.12.0";
93
136
 
94
137
  // src/transport/errors.ts
95
138
  var DreameError = class extends Error {
@@ -474,6 +517,34 @@ async function postForToken(ctx, body) {
474
517
  return session;
475
518
  }
476
519
 
520
+ // src/cloud/connectivity.ts
521
+ var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
522
+ var numOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
523
+ function parseBroker(bindDomain) {
524
+ const s = str(bindDomain);
525
+ if (!s) return null;
526
+ const idx = s.lastIndexOf(":");
527
+ if (idx < 0) return { host: s, port: null };
528
+ const port = Number(s.slice(idx + 1));
529
+ return { host: s.slice(0, idx), port: Number.isFinite(port) ? port : null };
530
+ }
531
+ function parseConnectivity(raw) {
532
+ const deviceInfo = raw["deviceInfo"] ?? {};
533
+ return {
534
+ online: raw["online"] === true,
535
+ connectionType: str(deviceInfo["scType"]),
536
+ mac: str(raw["mac"]),
537
+ broker: parseBroker(raw["bindDomain"]),
538
+ region: str(raw["region"]),
539
+ cloudVendor: str(raw["vendor"]),
540
+ firmwareVersion: str(raw["ver"]),
541
+ serialNumber: str(raw["sn"]),
542
+ subModel: str(raw["subModel"]),
543
+ battery: numOrNull(raw["battery"]),
544
+ statusCode: numOrNull(raw["latestStatus"])
545
+ };
546
+ }
547
+
477
548
  // src/cloud/devices.ts
478
549
  async function listDevices(input) {
479
550
  const ctx = input.ctx ?? RequestContext.from({ ...input, host: input.apiHost });
@@ -518,7 +589,8 @@ function toDevice(rawIn) {
518
589
  name,
519
590
  online,
520
591
  raw,
521
- cloudState: parseCloudState(raw)
592
+ cloudState: parseCloudState(raw),
593
+ connectivity: parseConnectivity(raw)
522
594
  };
523
595
  if (rawIn.mac) {
524
596
  device.mac = String(rawIn.mac);
@@ -2103,8 +2175,1032 @@ var VacuumCapabilityResolver = class {
2103
2175
  }
2104
2176
  };
2105
2177
 
2106
- // src/models/vacuum/map/envelope.ts
2178
+ // src/video/schemas.ts
2179
+ var import_zod3 = require("zod");
2180
+ var VideoAccessTokenResponseSchema = import_zod3.z.object({
2181
+ code: import_zod3.z.number().optional(),
2182
+ msg: import_zod3.z.string().nullish(),
2183
+ data: import_zod3.z.object({
2184
+ data: import_zod3.z.object({
2185
+ token: import_zod3.z.string(),
2186
+ userId: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional(),
2187
+ expireAt: import_zod3.z.number().optional()
2188
+ }).passthrough()
2189
+ }).passthrough()
2190
+ }).passthrough();
2191
+ var AuthCodeResponseSchema = import_zod3.z.object({
2192
+ code: import_zod3.z.number().optional(),
2193
+ msg: import_zod3.z.string().nullish(),
2194
+ data: import_zod3.z.string()
2195
+ }).passthrough();
2196
+ var FamilyIdResponseSchema = import_zod3.z.object({
2197
+ code: import_zod3.z.number().optional(),
2198
+ msg: import_zod3.z.string().nullish(),
2199
+ data: import_zod3.z.object({ data: import_zod3.z.object({ familyId: import_zod3.z.string() }).passthrough() }).passthrough()
2200
+ }).passthrough();
2201
+ var DeviceVideoInfoSchema = import_zod3.z.object({
2202
+ model: import_zod3.z.string().optional(),
2203
+ displayName: import_zod3.z.string().optional(),
2204
+ permit: import_zod3.z.string().optional(),
2205
+ videoDynamicVendor: import_zod3.z.boolean().optional(),
2206
+ defaultVendors: import_zod3.z.array(import_zod3.z.string()).optional(),
2207
+ productId: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional()
2208
+ }).passthrough();
2209
+ var DeviceInfoRecordSchema = import_zod3.z.object({
2210
+ did: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number()]).optional(),
2211
+ model: import_zod3.z.string().optional(),
2212
+ customName: import_zod3.z.string().optional(),
2213
+ vendor: import_zod3.z.string().optional(),
2214
+ online: import_zod3.z.boolean().optional(),
2215
+ lwt: import_zod3.z.number().optional(),
2216
+ property: import_zod3.z.string().optional(),
2217
+ bindDomain: import_zod3.z.string().optional(),
2218
+ deviceInfo: DeviceVideoInfoSchema.optional()
2219
+ }).passthrough();
2220
+ var DeviceInfoResponseSchema = import_zod3.z.object({
2221
+ code: import_zod3.z.number().optional(),
2222
+ msg: import_zod3.z.string().nullish(),
2223
+ data: DeviceInfoRecordSchema
2224
+ }).passthrough();
2225
+
2226
+ // src/video/profile.ts
2227
+ var KNOWN_VENDORS = ["tx", "ali"];
2228
+ function asVendor(v) {
2229
+ return v === "tx" || v === "ali" ? v : null;
2230
+ }
2231
+ function parseProperty(property) {
2232
+ if (!property) {
2233
+ return { iotId: null, lwt: null };
2234
+ }
2235
+ try {
2236
+ const p = JSON.parse(property);
2237
+ return {
2238
+ iotId: typeof p.iotId === "string" && p.iotId.length > 0 ? p.iotId : null,
2239
+ lwt: typeof p.lwt === "number" ? p.lwt : null
2240
+ };
2241
+ } catch {
2242
+ return { iotId: null, lwt: null };
2243
+ }
2244
+ }
2245
+ function toVideoProfile(record) {
2246
+ const info = record.deviceInfo ?? {};
2247
+ const supportedVendors = (info.defaultVendors ?? []).filter(
2248
+ (v) => KNOWN_VENDORS.includes(v)
2249
+ );
2250
+ const prop = parseProperty(record.property);
2251
+ const online = record.online === true || record.lwt === 1 || prop.lwt === 1;
2252
+ return {
2253
+ did: String(record.did ?? ""),
2254
+ model: String(record.model ?? info.model ?? ""),
2255
+ displayName: info.displayName ?? null,
2256
+ videoCapable: info.permit === "video" || supportedVendors.length > 0,
2257
+ currentVendor: asVendor(record.vendor),
2258
+ supportedVendors,
2259
+ dynamicVendor: info.videoDynamicVendor === true,
2260
+ iotId: prop.iotId,
2261
+ online
2262
+ };
2263
+ }
2264
+
2265
+ // src/video/client.ts
2266
+ var P_THIRD_VIDEO = "/dreame-third-video";
2267
+ var P_SMARTHOME = "/dreame-smarthome";
2268
+ var P_USER_IOT = "/dreame-user-iot";
2269
+ var DEFAULT_OS = "android";
2270
+ function resolveCtx(input) {
2271
+ return input.ctx ?? RequestContext.from({ ...input, host: input.apiHost });
2272
+ }
2273
+ function passthrough(input) {
2274
+ return {
2275
+ accessToken: input.session.accessToken,
2276
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
2277
+ ...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
2278
+ };
2279
+ }
2280
+ async function getVideoAccessToken(input) {
2281
+ const ctx = resolveCtx(input);
2282
+ const raw = await httpPostJsonBody({
2283
+ ctx,
2284
+ path: `${P_THIRD_VIDEO}/tx/user/accesstoken`,
2285
+ body: { os: DEFAULT_OS },
2286
+ context: "video accesstoken",
2287
+ ...passthrough(input)
2288
+ });
2289
+ const parsed = VideoAccessTokenResponseSchema.parse(raw);
2290
+ const d = parsed.data.data;
2291
+ return {
2292
+ token: d.token,
2293
+ userId: d.userId !== void 0 ? String(d.userId) : null,
2294
+ // Cloud reports expiry in epoch-seconds; expose epoch-ms like DreameSession.
2295
+ expiresAt: d.expireAt !== void 0 ? d.expireAt * 1e3 : 0
2296
+ };
2297
+ }
2298
+ async function getAliyunAuthCode(input) {
2299
+ const ctx = resolveCtx(input);
2300
+ const raw = await httpPostJsonBody({
2301
+ ctx,
2302
+ path: `${P_SMARTHOME}/aliIot/getAuthCodeV3`,
2303
+ body: { os: DEFAULT_OS },
2304
+ context: "aliyun authCode",
2305
+ ...passthrough(input)
2306
+ });
2307
+ return AuthCodeResponseSchema.parse(raw).data;
2308
+ }
2309
+ async function getVideoFamilyId(input) {
2310
+ const ctx = resolveCtx(input);
2311
+ const videoToken = input.videoToken ?? (await getVideoAccessToken({ ...input, ctx })).token;
2312
+ const raw = await httpPostJsonBody({
2313
+ ctx,
2314
+ path: `${P_THIRD_VIDEO}/tx/mgr/family/getFamilyId`,
2315
+ body: { accesstoken: videoToken, os: DEFAULT_OS },
2316
+ context: "video familyId",
2317
+ ...passthrough(input)
2318
+ });
2319
+ return FamilyIdResponseSchema.parse(raw).data.data.familyId;
2320
+ }
2321
+ async function getDeviceVideoProfile(input) {
2322
+ const ctx = resolveCtx(input);
2323
+ const raw = await httpPostJsonBody({
2324
+ ctx,
2325
+ path: `${P_USER_IOT}/iotuserbind/device/info`,
2326
+ body: { did: input.did },
2327
+ context: "device video info",
2328
+ ...passthrough(input)
2329
+ });
2330
+ return toVideoProfile(DeviceInfoResponseSchema.parse(raw).data);
2331
+ }
2332
+
2333
+ // src/video/aliyun/constants.ts
2334
+ var ALIYUN_APP_KEY = "33974273";
2335
+ var ALIYUN_APP_SECRET = "65a62f03ae5eda97af962fecc2e3efa6";
2336
+ var ALIYUN_REGION_ID = {
2337
+ eu: "eu-central-1",
2338
+ de: "eu-central-1",
2339
+ us: "us-east-1",
2340
+ cn: "cn-shanghai",
2341
+ ru: "eu-central-1",
2342
+ sg: "ap-southeast-1",
2343
+ in: "ap-southeast-1",
2344
+ tw: "cn-shanghai"
2345
+ };
2346
+ function livingAccountHost(regionId) {
2347
+ return `living-account.${regionId}.aliyuncs.com`;
2348
+ }
2349
+ function apiIotHost(regionId) {
2350
+ return `${regionId}.api-iot.aliyuncs.com`;
2351
+ }
2352
+ var VISION_STREAM_QUERY = "/vision/customer/stream/query";
2353
+
2354
+ // src/video/aliyun/identity.ts
2355
+ var import_node_crypto3 = require("crypto");
2356
+ var import_zod4 = require("zod");
2357
+
2358
+ // src/video/aliyun/signing.ts
2107
2359
  var import_node_crypto2 = require("crypto");
2360
+ var CT_JSON = "application/json; charset=UTF-8";
2361
+ var CT_FORM = "application/x-www-form-urlencoded; charset=UTF-8";
2362
+ function md5Base64(s) {
2363
+ return (0, import_node_crypto2.createHash)("md5").update(s, "utf8").digest("base64");
2364
+ }
2365
+ function signApiGatewayRequest(input) {
2366
+ const accept = CT_JSON;
2367
+ const contentType = input.json !== void 0 ? CT_JSON : CT_FORM;
2368
+ const date = input.date ?? (/* @__PURE__ */ new Date()).toUTCString();
2369
+ let body;
2370
+ let contentMd5 = "";
2371
+ let formParams;
2372
+ if (input.json !== void 0) {
2373
+ body = JSON.stringify(input.json);
2374
+ contentMd5 = md5Base64(body);
2375
+ } else {
2376
+ const form = input.form ?? {};
2377
+ body = Object.entries(form).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
2378
+ formParams = form;
2379
+ }
2380
+ const caHeaders = {
2381
+ "X-Ca-Key": input.appKey,
2382
+ "X-Ca-Nonce": input.nonce ?? crypto.randomUUID().toUpperCase(),
2383
+ "X-Ca-Timestamp": String(input.timestamp ?? Date.now()),
2384
+ "X-Ca-Version": "1",
2385
+ ...input.extraCaHeaders ?? {}
2386
+ };
2387
+ const signedHeaderNames = Object.keys(caHeaders).sort(
2388
+ (a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0
2389
+ );
2390
+ const headerBlock = signedHeaderNames.map((k) => `${k}:${caHeaders[k]}
2391
+ `).join("");
2392
+ let resource = input.path;
2393
+ if (formParams && Object.keys(formParams).length > 0) {
2394
+ const sorted = Object.keys(formParams).sort();
2395
+ resource += "?" + sorted.map((k) => `${k}=${formParams[k]}`).join("&");
2396
+ }
2397
+ const stringToSign = `${input.method}
2398
+ ${accept}
2399
+ ${contentMd5}
2400
+ ${contentType}
2401
+ ${date}
2402
+ ${headerBlock}${resource}`;
2403
+ const signature = (0, import_node_crypto2.createHmac)("sha1", input.appSecret).update(stringToSign, "utf8").digest("base64");
2404
+ const headers = {
2405
+ "Content-Type": contentType,
2406
+ Accept: accept,
2407
+ Date: date,
2408
+ "X-Ca-Signature": signature,
2409
+ "X-Ca-Signature-Method": "HmacSHA1",
2410
+ "X-Ca-Signature-Headers": signedHeaderNames.join(","),
2411
+ ...caHeaders
2412
+ };
2413
+ if (contentMd5) {
2414
+ headers["Content-MD5"] = contentMd5;
2415
+ }
2416
+ return { headers, body };
2417
+ }
2418
+
2419
+ // src/video/aliyun/identity.ts
2420
+ var LoginByOauthSchema = import_zod4.z.object({
2421
+ data: import_zod4.z.object({
2422
+ data: import_zod4.z.object({
2423
+ loginSuccessResult: import_zod4.z.object({
2424
+ sid: import_zod4.z.string(),
2425
+ refreshToken: import_zod4.z.string().optional(),
2426
+ token: import_zod4.z.string().optional(),
2427
+ openAccount: import_zod4.z.object({ openId: import_zod4.z.string().optional() }).passthrough().optional()
2428
+ }).passthrough()
2429
+ }).passthrough()
2430
+ }).passthrough()
2431
+ }).passthrough();
2432
+ var CreateSessionSchema = import_zod4.z.object({
2433
+ code: import_zod4.z.number().optional(),
2434
+ message: import_zod4.z.string().nullish(),
2435
+ data: import_zod4.z.object({
2436
+ iotToken: import_zod4.z.string(),
2437
+ refreshToken: import_zod4.z.string().optional(),
2438
+ identityId: import_zod4.z.string().optional(),
2439
+ iotTokenExpire: import_zod4.z.number().optional()
2440
+ }).passthrough()
2441
+ }).passthrough();
2442
+ async function postJson(url, headers, body, fetchImpl, ctx) {
2443
+ const res = await fetchImpl(url, { method: "POST", headers, body });
2444
+ const text = await res.text();
2445
+ try {
2446
+ return JSON.parse(text);
2447
+ } catch {
2448
+ throw new DreameApiError(`${ctx} returned non-JSON (status ${res.status})`, res.status);
2449
+ }
2450
+ }
2451
+ async function loginByOauth(input) {
2452
+ const host = livingAccountHost(input.regionId);
2453
+ const riskControlInfo = {
2454
+ createIfNotExist: "true",
2455
+ sdkVersion: "3.4.0.27",
2456
+ openaccountVersion: "3.4.0.27",
2457
+ platformName: "iOS",
2458
+ netType: "wifi",
2459
+ brand: "Apple",
2460
+ USE_H5_NC: "true",
2461
+ deviceId: input.deviceId ?? (0, import_node_crypto3.randomUUID)(),
2462
+ locale: "en-US",
2463
+ platformVersion: "26.5.2",
2464
+ platform: "iPhone",
2465
+ appKey: ALIYUN_APP_KEY,
2466
+ USE_OA_PWD_ENCRYPT: "true",
2467
+ model: "iPhone16,2",
2468
+ utdid: input.utdid ?? (0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 20),
2469
+ appVersion: "1895"
2470
+ };
2471
+ const value = JSON.stringify({
2472
+ riskControlInfo,
2473
+ oauthPlateform: 23,
2474
+ oauthAppKey: ALIYUN_APP_KEY,
2475
+ authCode: input.authCode
2476
+ });
2477
+ const signed = signApiGatewayRequest({
2478
+ method: "POST",
2479
+ host,
2480
+ path: "/api/prd/loginbyoauth.json",
2481
+ appKey: ALIYUN_APP_KEY,
2482
+ appSecret: ALIYUN_APP_SECRET,
2483
+ form: { loginByOauthRequest: value }
2484
+ });
2485
+ const json = await postJson(
2486
+ `https://${host}/api/prd/loginbyoauth.json`,
2487
+ signed.headers,
2488
+ signed.body,
2489
+ input.fetchImpl ?? defaultFetch,
2490
+ "loginbyoauth"
2491
+ );
2492
+ const r = LoginByOauthSchema.parse(json).data.data.loginSuccessResult;
2493
+ return {
2494
+ sid: r.sid,
2495
+ refreshToken: r.refreshToken ?? "",
2496
+ openId: r.openAccount?.openId ?? ""
2497
+ };
2498
+ }
2499
+ async function createIotSession(input) {
2500
+ const host = apiIotHost(input.regionId);
2501
+ const path = "/account/createSessionByAuthCode";
2502
+ const payload = {
2503
+ id: (0, import_node_crypto3.randomUUID)().toUpperCase(),
2504
+ params: { request: { appKey: ALIYUN_APP_KEY, authCode: input.sid, accountType: "OA_SESSION" } },
2505
+ request: { apiVer: "1.0.4", language: "en-US", appKey: ALIYUN_APP_KEY },
2506
+ version: "1.0.0"
2507
+ };
2508
+ const signed = signApiGatewayRequest({
2509
+ method: "POST",
2510
+ host,
2511
+ path,
2512
+ appKey: ALIYUN_APP_KEY,
2513
+ appSecret: ALIYUN_APP_SECRET,
2514
+ json: payload,
2515
+ extraCaHeaders: { "X-Ca-Stage": "RELEASE" }
2516
+ });
2517
+ const json = await postJson(
2518
+ `https://${host}${path}`,
2519
+ signed.headers,
2520
+ signed.body,
2521
+ input.fetchImpl ?? defaultFetch,
2522
+ "createSessionByAuthCode"
2523
+ );
2524
+ const parsed = CreateSessionSchema.parse(json);
2525
+ if (parsed.code !== 200 && parsed.code !== 0) {
2526
+ throw new DreameApiError(`createSession rejected: code=${parsed.code} msg=${parsed.message ?? "?"}`, 200, parsed);
2527
+ }
2528
+ const d = parsed.data;
2529
+ return {
2530
+ iotToken: d.iotToken,
2531
+ refreshToken: d.refreshToken ?? "",
2532
+ identityId: d.identityId ?? "",
2533
+ expiresAt: Date.now() + (d.iotTokenExpire ?? 72e3) * 1e3
2534
+ };
2535
+ }
2536
+
2537
+ // src/video/aliyun/vision.ts
2538
+ var import_zod5 = require("zod");
2539
+ var VISION_DEVICE_OFFLINE = 9201;
2540
+ var StreamQueryResponseSchema = import_zod5.z.object({
2541
+ code: import_zod5.z.number().optional(),
2542
+ message: import_zod5.z.string().nullish(),
2543
+ data: import_zod5.z.object({
2544
+ typeList: import_zod5.z.array(import_zod5.z.string()).optional(),
2545
+ relayUrl: import_zod5.z.string().optional(),
2546
+ relayDecryptKey: import_zod5.z.object({ iv: import_zod5.z.string().optional(), key: import_zod5.z.string().optional() }).passthrough().optional(),
2547
+ p2pInfo: import_zod5.z.object({
2548
+ stunUrl: import_zod5.z.string().optional(),
2549
+ alterStunUrl: import_zod5.z.string().optional(),
2550
+ signalUrl: import_zod5.z.string().optional(),
2551
+ portDetectStunUrlList: import_zod5.z.array(import_zod5.z.string()).optional()
2552
+ }).passthrough().optional(),
2553
+ supportVisionPtz: import_zod5.z.boolean().optional()
2554
+ }).passthrough().optional()
2555
+ }).passthrough();
2556
+ async function streamQuery(input) {
2557
+ const appKey = input.appKey ?? ALIYUN_APP_KEY;
2558
+ const appSecret = input.appSecret ?? ALIYUN_APP_SECRET;
2559
+ const host = apiIotHost(input.regionId);
2560
+ const payload = {
2561
+ id: crypto.randomUUID().toUpperCase(),
2562
+ params: {
2563
+ streamType: input.streamType ?? 0,
2564
+ needDomainName: true,
2565
+ relayEncryptType: 0,
2566
+ clientType: "iOS",
2567
+ iotId: input.iotId,
2568
+ enablePortPredict: true,
2569
+ enableWebSocket: true,
2570
+ forceIFrame: true,
2571
+ cacheDuration: 3e3,
2572
+ relayEncrypted: input.relayEncrypted ?? false
2573
+ },
2574
+ request: { language: "en-US", appKey, iotToken: input.iotToken, apiVer: "2.2.0" },
2575
+ version: "1.0.0"
2576
+ };
2577
+ const signed = signApiGatewayRequest({
2578
+ method: "POST",
2579
+ host,
2580
+ path: VISION_STREAM_QUERY,
2581
+ appKey,
2582
+ appSecret,
2583
+ json: payload,
2584
+ extraCaHeaders: { "X-Ca-Stage": "RELEASE" }
2585
+ });
2586
+ const fetchImpl = input.fetchImpl ?? defaultFetch;
2587
+ const res = await fetchImpl(`https://${host}${VISION_STREAM_QUERY}`, {
2588
+ method: "POST",
2589
+ headers: signed.headers,
2590
+ body: signed.body,
2591
+ ...input.signal ? { signal: input.signal } : {}
2592
+ });
2593
+ const text = await res.text();
2594
+ let json;
2595
+ try {
2596
+ json = JSON.parse(text);
2597
+ } catch {
2598
+ throw new DreameApiError(`stream/query returned non-JSON (status ${res.status})`, res.status);
2599
+ }
2600
+ const parsed = StreamQueryResponseSchema.parse(json);
2601
+ if (parsed.code === VISION_DEVICE_OFFLINE) {
2602
+ throw new DreameDeviceOfflineError(
2603
+ `stream/query: device video agent offline (code ${VISION_DEVICE_OFFLINE}) \u2014 retry to wake`,
2604
+ res.status,
2605
+ parsed
2606
+ );
2607
+ }
2608
+ if (parsed.code !== 200 && parsed.code !== 0) {
2609
+ throw new DreameApiError(
2610
+ `stream/query rejected: code=${parsed.code} msg=${parsed.message ?? "?"}`,
2611
+ res.status,
2612
+ parsed
2613
+ );
2614
+ }
2615
+ const d = parsed.data ?? {};
2616
+ const dk = d.relayDecryptKey;
2617
+ const hasKey = dk && dk.iv && dk.key;
2618
+ const p = d.p2pInfo;
2619
+ const p2pInfo = p ? {
2620
+ ...p.stunUrl !== void 0 ? { stunUrl: p.stunUrl } : {},
2621
+ ...p.alterStunUrl !== void 0 ? { alterStunUrl: p.alterStunUrl } : {},
2622
+ ...p.signalUrl !== void 0 ? { signalUrl: p.signalUrl } : {},
2623
+ ...p.portDetectStunUrlList !== void 0 ? { portDetectStunUrlList: p.portDetectStunUrlList } : {}
2624
+ } : null;
2625
+ return {
2626
+ typeList: d.typeList ?? [],
2627
+ relayUrl: d.relayUrl ?? null,
2628
+ relayDecryptKey: hasKey ? { iv: dk.iv, key: dk.key } : null,
2629
+ p2pInfo,
2630
+ ...d.supportVisionPtz !== void 0 ? { supportVisionPtz: d.supportVisionPtz } : {}
2631
+ };
2632
+ }
2633
+
2634
+ // src/video/aliyun/session.ts
2635
+ var DEFAULT_WAKE_TIMEOUT_MS = 3e4;
2636
+ var DEFAULT_WAKE_POLL_MS = 2e3;
2637
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2638
+ var REFRESH_LEEWAY_MS = 6e4;
2639
+ var DreameVideoSession = class {
2640
+ #session;
2641
+ #region;
2642
+ #regionId;
2643
+ #fetchImpl;
2644
+ #iot = null;
2645
+ #inFlight = null;
2646
+ constructor(input) {
2647
+ this.#session = input.session;
2648
+ this.#region = input.region;
2649
+ this.#regionId = ALIYUN_REGION_ID[input.region];
2650
+ this.#fetchImpl = input.fetchImpl;
2651
+ }
2652
+ /** The resolved Aliyun IoT region id (e.g. `eu-central-1`). */
2653
+ get regionId() {
2654
+ return this.#regionId;
2655
+ }
2656
+ /** Mint (or reuse) a valid Aliyun IoT session, refreshing before expiry. */
2657
+ async ensureIotSession() {
2658
+ const cur = this.#iot;
2659
+ if (cur && Date.now() < cur.expiresAt - REFRESH_LEEWAY_MS) {
2660
+ return cur;
2661
+ }
2662
+ if (this.#inFlight) {
2663
+ return this.#inFlight;
2664
+ }
2665
+ this.#inFlight = this.#mint();
2666
+ try {
2667
+ this.#iot = await this.#inFlight;
2668
+ return this.#iot;
2669
+ } finally {
2670
+ this.#inFlight = null;
2671
+ }
2672
+ }
2673
+ async #mint() {
2674
+ const base = {
2675
+ session: this.#session,
2676
+ region: this.#region,
2677
+ ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
2678
+ };
2679
+ const authCode = await getAliyunAuthCode(base);
2680
+ const oa = await loginByOauth({
2681
+ authCode,
2682
+ regionId: this.#regionId,
2683
+ ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
2684
+ });
2685
+ return createIotSession({
2686
+ sid: oa.sid,
2687
+ regionId: this.#regionId,
2688
+ ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
2689
+ });
2690
+ }
2691
+ /**
2692
+ * Mint a fresh LinkVisual stream descriptor for a device. `encrypted:false`
2693
+ * (default) yields a plain RTMP relay URL directly playable by ffmpeg.
2694
+ *
2695
+ * A camera that has been idle reports "device offline" until the query wakes
2696
+ * its video agent via the LinkVisual cloud. This transparently retries on that
2697
+ * (up to `wakeTimeoutMs`, default 30s) so a single call returns a live stream;
2698
+ * pass `wakeTimeoutMs: 0` to fail fast instead.
2699
+ */
2700
+ async getStreamInfo(iotId, opts) {
2701
+ const deadline = Date.now() + (opts?.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS);
2702
+ const pollMs = opts?.pollMs ?? DEFAULT_WAKE_POLL_MS;
2703
+ for (; ; ) {
2704
+ const iot = await this.ensureIotSession();
2705
+ try {
2706
+ return await streamQuery({
2707
+ regionId: this.#regionId,
2708
+ iotToken: iot.iotToken,
2709
+ iotId,
2710
+ relayEncrypted: opts?.encrypted ?? false,
2711
+ ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
2712
+ });
2713
+ } catch (err) {
2714
+ if (err instanceof DreameDeviceOfflineError && Date.now() + pollMs < deadline) {
2715
+ await sleep(pollMs);
2716
+ continue;
2717
+ }
2718
+ throw err;
2719
+ }
2720
+ }
2721
+ }
2722
+ /**
2723
+ * Convenience: mint a fresh plain RTMP relay URL for a device, or `null` when
2724
+ * the device is not currently streaming (no relay available).
2725
+ */
2726
+ async getRtmpUrl(iotId) {
2727
+ const info = await this.getStreamInfo(iotId, { encrypted: false });
2728
+ return info.relayUrl;
2729
+ }
2730
+ };
2731
+
2732
+ // src/video/camera-availability.ts
2733
+ function explainNoCameraChannel(profile) {
2734
+ if (profile.iotId) {
2735
+ return null;
2736
+ }
2737
+ const who = `${profile.model || "unknown model"} (${profile.did || "no did"})`;
2738
+ if (!profile.videoCapable) {
2739
+ return `${who} is not a camera: the cloud lists no video permit and no supported video vendor for it, so there is no channel to open.`;
2740
+ }
2741
+ if (!profile.online) {
2742
+ return `${who} is a camera but is OFFLINE. The video binding is published by the device itself, so there is nothing to read until it connects \u2014 this is not a provisioning fault.`;
2743
+ }
2744
+ const on = profile.currentVendor ?? "none";
2745
+ const supports = profile.supportedVendors.length > 0 ? profile.supportedVendors.join(", ") : "none";
2746
+ return `${who} is a camera and online, but has no video channel yet (vendor: ${on}; supports: ${supports}). The device has not bound to the video service \u2014 open its camera once in the Dreame app to create the binding, then retry.`;
2747
+ }
2748
+
2749
+ // src/video/monitor/constants.ts
2750
+ var MONITOR_SIID = 10001;
2751
+ var MONITOR_AIID = {
2752
+ /** Stream lifecycle + keep-alive (monitor start/end, keep_alive). */
2753
+ CAMERA_OPERATE: 1,
2754
+ /** Two-way intercom start/end. */
2755
+ VOICE_OPERATE: 2,
2756
+ /** Generic property-scoped operations. */
2757
+ PROPERTY_OPERATE: 3,
2758
+ /** Privacy access-code gate (launch/verify/set/reset). */
2759
+ ACCESS_CODE_OPERATE: 4,
2760
+ /** Video-vendor SDK init / switch. */
2761
+ VIDEO_VENDOR: 7
2762
+ };
2763
+ var MONITOR_PIID = {
2764
+ MONITOR_STATUS: 1,
2765
+ MONITOR_AUDIO_STATUS: 2,
2766
+ MONITOR_RECORD_STATUS: 4,
2767
+ TAKE_PHOTO: 5,
2768
+ KEEP_ALIVE: 6,
2769
+ MONITOR_FAULTS: 7,
2770
+ FILL_LIGHT_SET: 9,
2771
+ CAMERA_LIGHT_SWITCH: 10,
2772
+ VIDEO_VENDOR_STATUS: 11,
2773
+ GET_PROPERTY: 99,
2774
+ PERSON_DATA: 110,
2775
+ VIDEO_OBSTACLE_SWITCH: 111,
2776
+ VIDEO_OBSTACLE_DATA: 112,
2777
+ MONITOR_TASK_STATUS: 103,
2778
+ UPLOAD_STATUS: 1003,
2779
+ GET_ACCESS_CODE: 1100,
2780
+ SET_ACCESS_CODE: 1101,
2781
+ VERIFY_ACCESS_CODE: 1102,
2782
+ MONITOR_REST_MODE: 1103,
2783
+ SET_ACCESS_HOT_STATUS: 1104,
2784
+ MONITOR_SPACE: 2003
2785
+ };
2786
+ var MONITOR_VENDOR_TOKEN = {
2787
+ ali: "alify",
2788
+ tx: "tx"
2789
+ };
2790
+ var VACUUM_SIID = 4;
2791
+ var VACUUM_MOVE = {
2792
+ /** Remote-drive state property (set_properties), sent ~1 Hz while held. */
2793
+ REMOTE_STATE_PIID: 15,
2794
+ /** Work-mode action instance (go-to-point, cruise, person-follow start). */
2795
+ WORK_AIID: 1,
2796
+ /** Work-mode stop action instance (person-follow stop). */
2797
+ STOP_AIID: 2,
2798
+ /** piid carrying the work-mode selector value in the WORK action. */
2799
+ MODE_PIID: 1,
2800
+ /** piid carrying the point-info JSON in the WORK action. */
2801
+ POINT_PIID: 10,
2802
+ /** Work-mode selector values. */
2803
+ MODE_SPOT: 23,
2804
+ // go to point (MonitorSpotMode)
2805
+ MODE_CRUISE: 22,
2806
+ // cruise / patrol / find-pet
2807
+ MODE_PERSON_FOLLOW: 26,
2808
+ MODE_SPOT_CLEAN: 20,
2809
+ // spot-clean the current position (SpotClean)
2810
+ /** Auto-switch setter property (fill-light auto/full toggle lives here). */
2811
+ AUTO_SWITCH_PIID: 50
2812
+ };
2813
+ var VACUUM_CHARGE = { siid: 3, aiid: 1 };
2814
+ var VOICE_SIID = 7;
2815
+ var VOICE_LOCATE_AIID = 1;
2816
+ var VOICE_PLAY_SOUND_AIID = 2;
2817
+ var VOICE_SOUND_PIID = 4;
2818
+ var VACUUM_LOCATE = { siid: VOICE_SIID, aiid: VOICE_LOCATE_AIID };
2819
+ var PET_SOUNDS = {
2820
+ meow: 684,
2821
+ bark: 685,
2822
+ footsteps: 686,
2823
+ purring: 687,
2824
+ tickTock: 688
2825
+ };
2826
+ var VACUUM_ACTIONS = {
2827
+ startClean: { siid: 2, aiid: 1 },
2828
+ pauseClean: { siid: 2, aiid: 2 },
2829
+ stopClean: { siid: 4, aiid: 2 },
2830
+ dockWash: { siid: 4, aiid: 4 },
2831
+ autoEmpty: { siid: 15, aiid: 1 }
2832
+ };
2833
+
2834
+ // src/video/monitor/protocol.ts
2835
+ var import_node_crypto4 = require("crypto");
2836
+ function makeMonitorSession(accountId, now = Date.now()) {
2837
+ return (0, import_node_crypto4.createHash)("md5").update(`${accountId}_${now}`).digest("hex");
2838
+ }
2839
+ function hashAccessCode(code) {
2840
+ return (0, import_node_crypto4.createHash)("sha256").update(code).digest("hex");
2841
+ }
2842
+ function buildActionInput(piid, params, session) {
2843
+ const value = JSON.stringify({ ...params ?? {}, session });
2844
+ return { piid, value };
2845
+ }
2846
+ function startMonitorParams(iotId, vendor, area) {
2847
+ return {
2848
+ token: MONITOR_VENDOR_TOKEN[vendor],
2849
+ channelId: iotId,
2850
+ area,
2851
+ operType: "monitor",
2852
+ operation: "start"
2853
+ };
2854
+ }
2855
+ function stopMonitorParams() {
2856
+ return { operType: "monitor", operation: "end" };
2857
+ }
2858
+ function keepAliveParams(videoStatus) {
2859
+ return { operType: "keep_alive", videoStatus };
2860
+ }
2861
+ function accessCodeLaunchParams() {
2862
+ return { open: true };
2863
+ }
2864
+ function verifyAccessCodeParams(code, lazymode = 0) {
2865
+ return { oldcode: hashAccessCode(code), lazymode };
2866
+ }
2867
+ function intercomStartParams(opts) {
2868
+ return {
2869
+ operType: "intercom",
2870
+ operation: "start",
2871
+ ...opts?.needRecordSound !== void 0 ? { need_record_sound: opts.needRecordSound } : {},
2872
+ ...opts?.videoCall ? { phone: 1 } : {}
2873
+ };
2874
+ }
2875
+ function intercomStopParams() {
2876
+ return { operType: "intercom", operation: "end" };
2877
+ }
2878
+ function fillLightParams(value) {
2879
+ return { value: String(value) };
2880
+ }
2881
+ function takePhotoParams() {
2882
+ return { operType: "takephoto", operation: "start" };
2883
+ }
2884
+ function remoteDriveValue(spdv, spdw, now = Date.now()) {
2885
+ return JSON.stringify({
2886
+ spdv,
2887
+ spdw,
2888
+ audio: "false",
2889
+ random: Math.floor(Math.random() * 1e3),
2890
+ timestamp: now
2891
+ });
2892
+ }
2893
+ var DRIVE_DIRECTIONS = {
2894
+ forward: [200, 0],
2895
+ left: [0, 45],
2896
+ right: [0, -45],
2897
+ turnAround: [0, 180],
2898
+ stop: [0, 0]
2899
+ };
2900
+ function actionCode(res) {
2901
+ const r = res;
2902
+ return typeof r?.code === "number" ? r.code : null;
2903
+ }
2904
+ function firstOutValue(res) {
2905
+ const r = res;
2906
+ const out = r?.out ?? r?.result?.out;
2907
+ return Array.isArray(out) && out.length > 0 ? out[0]?.value : void 0;
2908
+ }
2909
+
2910
+ // src/video/monitor/controller.ts
2911
+ var DEFAULT_KEEP_ALIVE_MS = 1e4;
2912
+ var DEFAULT_AREA = "4";
2913
+ var DreameCameraController = class {
2914
+ #device;
2915
+ #relay;
2916
+ #iotId;
2917
+ #accountId;
2918
+ #vendor;
2919
+ #accessCode;
2920
+ #area;
2921
+ #keepAliveIntervalMs;
2922
+ #wakeTimeoutMs;
2923
+ #onKeepAliveError;
2924
+ #log;
2925
+ #session = null;
2926
+ #keepAliveTimer = null;
2927
+ #opened = false;
2928
+ constructor(input) {
2929
+ this.#device = input.device;
2930
+ this.#relay = input.relay;
2931
+ this.#iotId = input.iotId;
2932
+ this.#accountId = input.accountId;
2933
+ this.#vendor = input.vendor ?? "ali";
2934
+ this.#accessCode = input.accessCode;
2935
+ this.#area = input.area ?? DEFAULT_AREA;
2936
+ this.#keepAliveIntervalMs = input.keepAliveIntervalMs ?? DEFAULT_KEEP_ALIVE_MS;
2937
+ this.#wakeTimeoutMs = input.wakeTimeoutMs;
2938
+ this.#onKeepAliveError = input.onKeepAliveError;
2939
+ this.#log = input.log ?? (() => {
2940
+ });
2941
+ }
2942
+ #logLine(msg) {
2943
+ this.#log(`[nodedreame:monitor] ${msg}`);
2944
+ }
2945
+ /** True while a monitor session is active (between {@link open} and {@link close}). */
2946
+ get isOpen() {
2947
+ return this.#opened;
2948
+ }
2949
+ /** Send one camera action, resolving to the raw device reply. */
2950
+ async #action(aiid, piid, params) {
2951
+ if (!this.#session) {
2952
+ throw new DreameError("camera controller has no session; call open() first");
2953
+ }
2954
+ const input = buildActionInput(piid, params, this.#session);
2955
+ return this.#device.callAction(MONITOR_SIID, aiid, [input]);
2956
+ }
2957
+ /**
2958
+ * Run the full cold-start sequence and return a live relay URL. Starts the
2959
+ * keep-alive loop; the caller MUST {@link close} when done to release the
2960
+ * device. Throws if the monitor cannot be started or no relay comes up.
2961
+ */
2962
+ async open() {
2963
+ if (this.#opened) {
2964
+ throw new DreameError("camera stream already open");
2965
+ }
2966
+ this.#session = makeMonitorSession(this.#accountId);
2967
+ this.#logLine(`open: vendor=${this.#vendor} iotId=${this.#iotId} area=${this.#area}`);
2968
+ await this.#tryPrime();
2969
+ const startRes = await this.#startMonitorWithGate();
2970
+ const encryptionKey = asString(firstOutValue(startRes));
2971
+ this.#logLine(`startMonitor accepted (encryptionKey=${encryptionKey ? "yes" : "no"})`);
2972
+ this.#opened = true;
2973
+ this.#startKeepAlive();
2974
+ let relayUrl;
2975
+ try {
2976
+ const info = await this.#relay.getStreamInfo(this.#iotId, {
2977
+ encrypted: false,
2978
+ ...this.#wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: this.#wakeTimeoutMs } : {}
2979
+ });
2980
+ relayUrl = info.relayUrl;
2981
+ } catch (err) {
2982
+ this.#logLine(`relay mint failed: ${err instanceof Error ? err.message : String(err)}`);
2983
+ await this.close();
2984
+ throw err;
2985
+ }
2986
+ if (!relayUrl) {
2987
+ await this.close();
2988
+ throw new DreameError("monitor started but no relay URL was returned");
2989
+ }
2990
+ this.#logLine("relay up");
2991
+ return { rtmpUrl: relayUrl, encryptionKey };
2992
+ }
2993
+ async #tryPrime() {
2994
+ try {
2995
+ await this.#action(MONITOR_AIID.ACCESS_CODE_OPERATE, MONITOR_PIID.GET_ACCESS_CODE, accessCodeLaunchParams());
2996
+ } catch {
2997
+ }
2998
+ try {
2999
+ await this.#action(MONITOR_AIID.VIDEO_VENDOR, MONITOR_PIID.VIDEO_VENDOR_STATUS, null);
3000
+ } catch {
3001
+ }
3002
+ }
3003
+ async #startMonitorWithGate() {
3004
+ const params = startMonitorParams(this.#iotId, this.#vendor, this.#area);
3005
+ let res = await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, params);
3006
+ if (actionCode(res) === 0) {
3007
+ return res;
3008
+ }
3009
+ this.#logLine(`startMonitor refused (code ${actionCode(res) ?? "?"}); verifying access code`);
3010
+ if (this.#accessCode) {
3011
+ await this.#action(
3012
+ MONITOR_AIID.ACCESS_CODE_OPERATE,
3013
+ MONITOR_PIID.VERIFY_ACCESS_CODE,
3014
+ verifyAccessCodeParams(this.#accessCode)
3015
+ );
3016
+ res = await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, params);
3017
+ }
3018
+ if (actionCode(res) !== 0) {
3019
+ const code = actionCode(res);
3020
+ throw new DreameError(
3021
+ `startMonitor refused (code ${code ?? "?"})` + (this.#accessCode ? "" : "; device likely requires an accessCode")
3022
+ );
3023
+ }
3024
+ return res;
3025
+ }
3026
+ #startKeepAlive() {
3027
+ this.#stopKeepAlive();
3028
+ this.#keepAliveTimer = setInterval(() => {
3029
+ void this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.KEEP_ALIVE, keepAliveParams("opened")).catch(
3030
+ (err) => this.#onKeepAliveError?.(err)
3031
+ );
3032
+ }, this.#keepAliveIntervalMs);
3033
+ this.#keepAliveTimer.unref?.();
3034
+ }
3035
+ #stopKeepAlive() {
3036
+ if (this.#keepAliveTimer) {
3037
+ clearInterval(this.#keepAliveTimer);
3038
+ this.#keepAliveTimer = null;
3039
+ }
3040
+ }
3041
+ /**
3042
+ * Mint a FRESH single-use RTMP relay URL for the already-open monitor. Each
3043
+ * consumer needs its own relay URL, so call this once per ffmpeg/consumer.
3044
+ * Throws if the stream is not open.
3045
+ */
3046
+ async mintRelayUrl() {
3047
+ if (!this.#opened) {
3048
+ throw new DreameError("camera stream is not open; call open() first");
3049
+ }
3050
+ const info = await this.#relay.getStreamInfo(this.#iotId, {
3051
+ encrypted: false,
3052
+ ...this.#wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: this.#wakeTimeoutMs } : {}
3053
+ });
3054
+ if (!info.relayUrl) {
3055
+ throw new DreameError("no relay URL available for the open monitor");
3056
+ }
3057
+ return info.relayUrl;
3058
+ }
3059
+ /** Start the two-way intercom session (control plane only; see module docs on mic uplink). */
3060
+ async startIntercom(opts) {
3061
+ return this.#action(MONITOR_AIID.VOICE_OPERATE, MONITOR_PIID.MONITOR_AUDIO_STATUS, intercomStartParams(opts));
3062
+ }
3063
+ /** Stop the two-way intercom session. */
3064
+ async stopIntercom() {
3065
+ return this.#action(MONITOR_AIID.VOICE_OPERATE, MONITOR_PIID.MONITOR_AUDIO_STATUS, intercomStopParams());
3066
+ }
3067
+ /** Set the fill-light brightness (40–100 = manual %, below 40 / 101 = auto). */
3068
+ async setFillLight(value) {
3069
+ return this.#action(MONITOR_AIID.PROPERTY_OPERATE, MONITOR_PIID.FILL_LIGHT_SET, fillLightParams(value));
3070
+ }
3071
+ /**
3072
+ * Device-side snapshot: the robot captures a still and uploads it to cloud
3073
+ * storage (read the result back from the `uploadStatus` property). Prefer a
3074
+ * frame-grab from the live stream for an instant image.
3075
+ */
3076
+ async takePhoto() {
3077
+ return this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.TAKE_PHOTO, takePhotoParams());
3078
+ }
3079
+ /**
3080
+ * Remote-drive the robot ("PTZ" substitute — the camera is fixed, the robot
3081
+ * moves). `spdv` = forward speed, `spdw` = turn; send repeatedly (~1 Hz) while
3082
+ * a direction is held, then `stop`. Only valid while the stream is open.
3083
+ */
3084
+ async drive(spdv, spdw) {
3085
+ this.#requireSession();
3086
+ if (!this.#device.setProperty) {
3087
+ throw new DreameError("device does not support setProperty; cannot remote-drive");
3088
+ }
3089
+ return this.#device.setProperty({
3090
+ siid: VACUUM_SIID,
3091
+ piid: VACUUM_MOVE.REMOTE_STATE_PIID,
3092
+ value: remoteDriveValue(spdv, spdw)
3093
+ });
3094
+ }
3095
+ /** Remote-drive by named direction (forward/left/right/turnAround/stop). */
3096
+ async driveDirection(direction) {
3097
+ const [spdv, spdw] = DRIVE_DIRECTIONS[direction];
3098
+ return this.drive(spdv, spdw);
3099
+ }
3100
+ /** Start person-follow mode (robot tracks a detected person). */
3101
+ async startPersonFollow() {
3102
+ this.#requireSession();
3103
+ return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
3104
+ { piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_PERSON_FOLLOW }
3105
+ ]);
3106
+ }
3107
+ /**
3108
+ * Universal stop for the current work mode — stops person-follow, spot-clean,
3109
+ * go-to-point, cruise, etc. (siid 4 / aiid 2).
3110
+ */
3111
+ async stopWork() {
3112
+ return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.STOP_AIID, []);
3113
+ }
3114
+ /** Stop person-follow mode (alias for {@link stopWork}). */
3115
+ async stopPersonFollow() {
3116
+ return this.stopWork();
3117
+ }
3118
+ /** Spot-clean the robot's current position (WorkMode SpotClean). */
3119
+ async spotClean() {
3120
+ return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
3121
+ { piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_SPOT_CLEAN }
3122
+ ]);
3123
+ }
3124
+ /** Play one of the robot's sound clips (e.g. pet sounds) by id. */
3125
+ async playSound(soundId) {
3126
+ return this.#device.callAction(VOICE_SIID, VOICE_PLAY_SOUND_AIID, [
3127
+ { piid: VOICE_SOUND_PIID, value: soundId }
3128
+ ]);
3129
+ }
3130
+ /** Play a named pet-teasing sound (meow/bark/footsteps/purring/tickTock). */
3131
+ async playPetSound(sound) {
3132
+ return this.playSound(PET_SOUNDS[sound]);
3133
+ }
3134
+ /** Run a common whole-robot action (startClean/pauseClean/stopClean/dockWash/autoEmpty). */
3135
+ async runVacuumAction(action) {
3136
+ const a = VACUUM_ACTIONS[action];
3137
+ return this.#device.callAction(a.siid, a.aiid, []);
3138
+ }
3139
+ /** Find-pet: cruise the home looking for the pet. */
3140
+ async findPet() {
3141
+ this.#requireSession();
3142
+ return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
3143
+ { piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_CRUISE },
3144
+ { piid: VACUUM_MOVE.POINT_PIID, value: JSON.stringify({ findpet: "" }) }
3145
+ ]);
3146
+ }
3147
+ /** Send the robot to a map point (`spoint` current, `tpoint` targets). */
3148
+ async goToPoint(spoint, tpoint) {
3149
+ this.#requireSession();
3150
+ return this.#device.callAction(VACUUM_SIID, VACUUM_MOVE.WORK_AIID, [
3151
+ { piid: VACUUM_MOVE.MODE_PIID, value: VACUUM_MOVE.MODE_SPOT },
3152
+ { piid: VACUUM_MOVE.POINT_PIID, value: JSON.stringify({ spoint, tpoint }) }
3153
+ ]);
3154
+ }
3155
+ /** Toggle the fill light between auto (`full=false`) and full-on (`full=true`). */
3156
+ async setFillLightAuto(full) {
3157
+ if (!this.#device.setProperty) {
3158
+ throw new DreameError("device does not support setProperty; cannot toggle fill light");
3159
+ }
3160
+ return this.#device.setProperty({
3161
+ siid: VACUUM_SIID,
3162
+ piid: VACUUM_MOVE.AUTO_SWITCH_PIID,
3163
+ value: JSON.stringify({ k: "FillinLight", v: full ? 1 : 0 })
3164
+ });
3165
+ }
3166
+ /**
3167
+ * Send the robot back to its dock to charge ("go home"). Works regardless of
3168
+ * stream state — usable as a standalone command or a PTZ "home" preset.
3169
+ */
3170
+ async returnToDock() {
3171
+ return this.#device.callAction(VACUUM_CHARGE.siid, VACUUM_CHARGE.aiid, []);
3172
+ }
3173
+ /** Locate the robot — it beeps. Works regardless of stream state. */
3174
+ async locate() {
3175
+ return this.#device.callAction(VACUUM_LOCATE.siid, VACUUM_LOCATE.aiid, []);
3176
+ }
3177
+ #requireSession() {
3178
+ if (!this.#opened) {
3179
+ throw new DreameError("camera stream is not open; call open() first");
3180
+ }
3181
+ }
3182
+ /** Stop the keep-alive loop and release the monitor on the device. Idempotent. */
3183
+ async close() {
3184
+ this.#stopKeepAlive();
3185
+ if (!this.#opened) {
3186
+ this.#session = null;
3187
+ return;
3188
+ }
3189
+ this.#opened = false;
3190
+ this.#logLine("close: stopping monitor");
3191
+ try {
3192
+ await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, stopMonitorParams());
3193
+ } finally {
3194
+ this.#session = null;
3195
+ }
3196
+ }
3197
+ };
3198
+ function asString(v) {
3199
+ return typeof v === "string" ? v : null;
3200
+ }
3201
+
3202
+ // src/models/vacuum/map/envelope.ts
3203
+ var import_node_crypto5 = require("crypto");
2108
3204
  var zlib = __toESM(require("zlib"), 1);
2109
3205
  var HEADER_SIZE = 27;
2110
3206
  var ANGLE_ABSENT = 32767;
@@ -2182,7 +3278,7 @@ function looksLikeBase64Zlib(buf) {
2182
3278
  }
2183
3279
  function aesCbcDecrypt(cipher, rawKey, iv) {
2184
3280
  const keyBytes = Buffer.from(
2185
- (0, import_node_crypto2.createHash)("sha256").update(rawKey).digest("hex").slice(0, 32),
3281
+ (0, import_node_crypto5.createHash)("sha256").update(rawKey).digest("hex").slice(0, 32),
2186
3282
  "utf8"
2187
3283
  );
2188
3284
  const ivBytes = Buffer.from(iv, "utf8");
@@ -2190,7 +3286,7 @@ function aesCbcDecrypt(cipher, rawKey, iv) {
2190
3286
  throw new MapDecodeError(`envelope: AES IV must be 16 ASCII bytes, got ${ivBytes.length}`);
2191
3287
  }
2192
3288
  try {
2193
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-cbc", keyBytes, ivBytes);
3289
+ const decipher = (0, import_node_crypto5.createDecipheriv)("aes-256-cbc", keyBytes, ivBytes);
2194
3290
  decipher.setAutoPadding(false);
2195
3291
  return Buffer.concat([decipher.update(cipher), decipher.final()]);
2196
3292
  } catch (err) {
@@ -3523,6 +4619,97 @@ function drawLabel(png, cx, cy, text, color, px) {
3523
4619
  }
3524
4620
  }
3525
4621
 
4622
+ // src/models/vacuum/map/wifi.ts
4623
+ var import_pngjs2 = require("pngjs");
4624
+ var NIBBLE_TO_BARS = { 10: 0, 11: 1, 12: 2, 13: 3, 14: 4 };
4625
+ var isWifiNibble = (n) => n >= 10 && n <= 14;
4626
+ var NIBBLE_RGBA = {
4627
+ 2: [176, 182, 190, 255],
4628
+ // map border (context)
4629
+ 3: [242, 244, 247, 255],
4630
+ // mapped floor (context)
4631
+ 10: [229, 234, 238, 255],
4632
+ // unreached
4633
+ 11: [217, 226, 239, 255],
4634
+ // 1 bar
4635
+ 12: [205, 218, 239, 255],
4636
+ // 2 bars
4637
+ 13: [161, 189, 242, 255],
4638
+ // 3 bars
4639
+ 14: [129, 168, 245, 255]
4640
+ // 4 bars
4641
+ };
4642
+ var poseOrNull = (x, y, a) => x === 0 && y === 0 ? null : { x, y, angle: a === ANGLE_ABSENT ? 0 : a };
4643
+ function decodeWifiSignalMap(input, opts = {}) {
4644
+ const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
4645
+ const header = parseMapHeader(inflated);
4646
+ const { width, height, gridSize, left, top } = header;
4647
+ const pixelEnd = HEADER_SIZE + width * height;
4648
+ const grid = inflated.subarray(HEADER_SIZE, pixelEnd);
4649
+ const cells = new Uint8Array(width * height);
4650
+ for (let i = 0; i < cells.length; i++) cells[i] = (grid[i] ?? 0) & 15;
4651
+ const dimensions = { left, top, width, height, gridSize };
4652
+ const robot = poseOrNull(header.robotX, header.robotY, header.robotA);
4653
+ const dock = poseOrNull(header.chargerX, header.chargerY, header.chargerA);
4654
+ const signalAt = (worldX, worldY) => {
4655
+ if (gridSize <= 0) return null;
4656
+ const col = Math.floor((worldX - left) / gridSize);
4657
+ const row = Math.floor((worldY - top) / gridSize);
4658
+ if (col < 0 || col >= width || row < 0 || row >= height) return null;
4659
+ const nibble = cells[row * width + col] ?? 0;
4660
+ return isWifiNibble(nibble) ? NIBBLE_TO_BARS[nibble] : null;
4661
+ };
4662
+ return {
4663
+ dimensions,
4664
+ robot,
4665
+ dock,
4666
+ cells,
4667
+ signalAt,
4668
+ currentSignal: robot ? signalAt(robot.x, robot.y) : null
4669
+ };
4670
+ }
4671
+ function renderWifiSignalPng(map, opts = {}) {
4672
+ const scale = Math.max(1, Math.floor(opts.scale ?? 4));
4673
+ const { width, height, left, top, gridSize } = map.dimensions;
4674
+ const png = new import_pngjs2.PNG({ width: width * scale, height: height * scale, colorType: 6 });
4675
+ for (let row = 0; row < height; row++) {
4676
+ for (let col = 0; col < width; col++) {
4677
+ const nibble = map.cells[row * width + col] ?? 0;
4678
+ const rgba = NIBBLE_RGBA[nibble];
4679
+ if (!rgba) continue;
4680
+ for (let dy = 0; dy < scale; dy++) {
4681
+ for (let dx = 0; dx < scale; dx++) {
4682
+ const px = col * scale + dx + (row * scale + dy) * width * scale;
4683
+ const o = px << 2;
4684
+ png.data[o] = rgba[0];
4685
+ png.data[o + 1] = rgba[1];
4686
+ png.data[o + 2] = rgba[2];
4687
+ png.data[o + 3] = rgba[3];
4688
+ }
4689
+ }
4690
+ }
4691
+ }
4692
+ if ((opts.markRobot ?? true) && map.robot && gridSize > 0) {
4693
+ const cx = Math.floor((map.robot.x - left) / gridSize) * scale + Math.floor(scale / 2);
4694
+ const cy = Math.floor((map.robot.y - top) / gridSize) * scale + Math.floor(scale / 2);
4695
+ const r = Math.max(2, scale);
4696
+ for (let dy = -r; dy <= r; dy++) {
4697
+ for (let dx = -r; dx <= r; dx++) {
4698
+ if (dx * dx + dy * dy > r * r) continue;
4699
+ const x = cx + dx;
4700
+ const y = cy + dy;
4701
+ if (x < 0 || y < 0 || x >= width * scale || y >= height * scale) continue;
4702
+ const o = x + y * width * scale << 2;
4703
+ png.data[o] = 255;
4704
+ png.data[o + 1] = 69;
4705
+ png.data[o + 2] = 0;
4706
+ png.data[o + 3] = 255;
4707
+ }
4708
+ }
4709
+ }
4710
+ return import_pngjs2.PNG.sync.write(png);
4711
+ }
4712
+
3526
4713
  // src/models/vacuum/map/oss-fetch.ts
3527
4714
  var OSS_DOWNLOAD_PATH = "/dreame-user-iot/iotfile/getDownloadUrl";
3528
4715
  var DEFAULT_TTL_MS = 30 * 60 * 1e3;
@@ -4008,11 +5195,11 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
4008
5195
  }
4009
5196
  this.#requireCap(this.#caps.canCleanPerRoom, "cleanZones", "per-room cleaning");
4010
5197
  const { repeats, fan, water } = this.#resolveCleanOpts(opts);
4011
- const areas = zones.map((z4) => [
4012
- Math.round(z4.x0),
4013
- Math.round(z4.y0),
4014
- Math.round(z4.x1),
4015
- Math.round(z4.y1),
5198
+ const areas = zones.map((z7) => [
5199
+ Math.round(z7.x0),
5200
+ Math.round(z7.y0),
5201
+ Math.round(z7.x1),
5202
+ Math.round(z7.y1),
4016
5203
  repeats,
4017
5204
  fan,
4018
5205
  water
@@ -4140,6 +5327,66 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
4140
5327
  ...input.iv !== void 0 ? { iv: input.iv } : {}
4141
5328
  };
4142
5329
  }
5330
+ /**
5331
+ * Fetch the robot's Wi-Fi signal map — the last stored coverage heatmap, with
5332
+ * NO robot movement. Requests it (map service `wifiMap` action, siid 6 aiid 4),
5333
+ * polls `PropWifiMap` (6/15) for the OSS object it advertises, fetches + decodes
5334
+ * it into a per-cell signal grid + geometry + robot/dock poses. The signal at
5335
+ * any world point (incl. the robot's own position) is a lookup — see
5336
+ * {@link WifiSignalMap.signalAt} / {@link WifiSignalMap.currentSignal}.
5337
+ *
5338
+ * Pass `requestFresh: false` to skip the request and read whatever is already
5339
+ * advertised. Throws if no Wi-Fi map exists yet (never run on this device).
5340
+ */
5341
+ async getWifiSignalMap(input = {}) {
5342
+ if (input.requestFresh !== false) {
5343
+ try {
5344
+ await this.callAction(6, 4, []);
5345
+ } catch {
5346
+ }
5347
+ }
5348
+ const pollMs = input.pollMs ?? 2500;
5349
+ const deadline = Date.now() + (input.maxWaitMs ?? 2e4);
5350
+ let objectName = "";
5351
+ for (; ; ) {
5352
+ const res = await this.refreshProperties([{ siid: 6, piid: 15 }]);
5353
+ const val = res[0]?.value;
5354
+ if (typeof val === "string" && val.length > 0) {
5355
+ objectName = val;
5356
+ break;
5357
+ }
5358
+ if (Date.now() + pollMs >= deadline) {
5359
+ throw new DreameError("wifi signal map not available (none stored yet, or request timed out)");
5360
+ }
5361
+ await new Promise((r) => setTimeout(r, pollMs));
5362
+ }
5363
+ const filename = objectName.replace(/^\d+,/, "");
5364
+ const blob = await this.#fetchMapBlob({
5365
+ filename,
5366
+ ...input.fetcher ? { fetcher: input.fetcher } : {},
5367
+ ...input.host ? { host: input.host } : {},
5368
+ ...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
5369
+ ...input.signal ? { signal: input.signal } : {}
5370
+ });
5371
+ return decodeWifiSignalMap(blob, this.#decodeOpts(input));
5372
+ }
5373
+ /**
5374
+ * Render the Wi-Fi signal map as a simple PNG heatmap image (weakest→strongest,
5375
+ * unreached grey, robot marker) — ready to display. Convenience over
5376
+ * {@link getWifiSignalMap} + {@link renderWifiSignalPng}.
5377
+ */
5378
+ async getWifiSignalImage(input = {}) {
5379
+ const map = await this.getWifiSignalMap(input);
5380
+ return renderWifiSignalPng(map, input);
5381
+ }
5382
+ /**
5383
+ * The one-call "current Wi-Fi signal": fetches the signal map and returns the
5384
+ * bars (1–4, 0 = unreached) at the robot's own position, or null when unknown.
5385
+ */
5386
+ async getCurrentSignal(input = {}) {
5387
+ const map = await this.getWifiSignalMap(input);
5388
+ return map.currentSignal;
5389
+ }
4143
5390
  /**
4144
5391
  * The merge base for live P-frame streaming — always an INFLATED frame buffer.
4145
5392
  * An I-frame (re)seeds it; each P-frame merge replaces it with the merged
@@ -4203,6 +5450,43 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
4203
5450
  this.emit("map", frame);
4204
5451
  return frame;
4205
5452
  }
5453
+ /**
5454
+ * Build a {@link DreameCameraController} for this device's camera, wired to
5455
+ * both planes it needs: the MIoT action transport (this device handle) and a
5456
+ * fresh Aliyun {@link DreameVideoSession} for the relay. Call
5457
+ * `controller.open()` to autonomously cold-start the stream and get a pullable
5458
+ * RTMP URL, then `controller.close()` to release it.
5459
+ *
5460
+ * The camera's per-session privacy gate means a coded camera needs its PIN:
5461
+ * pass `accessCode` (the code set at pairing). `iotId` is resolved from the
5462
+ * device's cloud video profile when not supplied.
5463
+ */
5464
+ async createCameraController(opts) {
5465
+ const session = this.currentSession();
5466
+ const region = this.region;
5467
+ const relay = opts?.relay ?? new DreameVideoSession({ session, region });
5468
+ let iotId = opts?.iotId ?? null;
5469
+ if (!iotId) {
5470
+ const profile = await getDeviceVideoProfile({ session, region, did: this.deviceId });
5471
+ iotId = profile.iotId;
5472
+ if (!iotId) {
5473
+ throw new DreameError(explainNoCameraChannel(profile) ?? "device has no video channel");
5474
+ }
5475
+ }
5476
+ return new DreameCameraController({
5477
+ device: this,
5478
+ relay,
5479
+ iotId,
5480
+ accountId: session.uid,
5481
+ ...opts?.vendor ? { vendor: opts.vendor } : {},
5482
+ ...opts?.accessCode !== void 0 ? { accessCode: opts.accessCode } : {},
5483
+ ...opts?.area ? { area: opts.area } : {},
5484
+ ...opts?.keepAliveIntervalMs !== void 0 ? { keepAliveIntervalMs: opts.keepAliveIntervalMs } : {},
5485
+ ...opts?.wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: opts.wakeTimeoutMs } : {},
5486
+ ...opts?.onKeepAliveError ? { onKeepAliveError: opts.onKeepAliveError } : {},
5487
+ ...opts?.log ? { log: opts.log } : {}
5488
+ });
5489
+ }
4206
5490
  /** Props worth seeding on start() / polling — exported for the facade. */
4207
5491
  static DEFAULT_PROPS = [
4208
5492
  VACUUM_PROP.STATE,
@@ -4741,7 +6025,7 @@ function isRecord3(v) {
4741
6025
  function asNumber(v, fallback) {
4742
6026
  return typeof v === "number" && Number.isFinite(v) ? v : fallback;
4743
6027
  }
4744
- function asString(v, fallback) {
6028
+ function asString2(v, fallback) {
4745
6029
  return typeof v === "string" ? v : fallback;
4746
6030
  }
4747
6031
  function mapIdFromIndex(mapIndex) {
@@ -4836,7 +6120,7 @@ function parseZones(list, includeTimes) {
4836
6120
  out.push({
4837
6121
  zoneId: asNumber(id, 0),
4838
6122
  path: extractPathCoords(data["path"]),
4839
- name: asString(data["name"], ""),
6123
+ name: asString2(data["name"], ""),
4840
6124
  zoneType: asNumber(data["type"], 0),
4841
6125
  shapeType: includeTimes ? asNumber(data["shapeType"], 0) : 0,
4842
6126
  area: includeTimes ? asNumber(data["area"], 0) : 0,
@@ -4857,7 +6141,7 @@ function parseSpotAreas(list) {
4857
6141
  out.push({
4858
6142
  areaId: Math.trunc(asNumber(id, 0)),
4859
6143
  path: extractPathCoords(data["path"]),
4860
- name: asString(data["name"], ""),
6144
+ name: asString2(data["name"], ""),
4861
6145
  shapeType: asNumber(data["shapeType"], 0),
4862
6146
  area: asNumber(data["area"], 0)
4863
6147
  });
@@ -4921,7 +6205,7 @@ function parseMowerMap(mapJsonStr) {
4921
6205
  contours: parseContours(parsePolygonList(data["contours"])),
4922
6206
  boundary: parseBoundary(data["boundary"]),
4923
6207
  totalArea: asNumber(data["totalArea"], 0),
4924
- name: asString(data["name"], ""),
6208
+ name: asString2(data["name"], ""),
4925
6209
  mapIndex,
4926
6210
  mapId: mapIdFromIndex(mapIndex),
4927
6211
  mowPaths: [],
@@ -5449,7 +6733,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5449
6733
  if (zoneIds.length === 0) {
5450
6734
  throw new RangeError("startMowingZones: zoneIds must not be empty");
5451
6735
  }
5452
- return this.#sendTask(buildZonePayload(zoneIds.map((z4) => Math.trunc(z4))));
6736
+ return this.#sendTask(buildZonePayload(zoneIds.map((z7) => Math.trunc(z7))));
5453
6737
  }
5454
6738
  /** Edge / contour mowing (2:50 o:101). Contour ids are two-int pairs [[1,0]]. */
5455
6739
  async startMowingEdges(contourIds) {
@@ -5805,10 +7089,13 @@ var Nodreame = class extends TypedEmitter {
5805
7089
  if (this.#closed) {
5806
7090
  return;
5807
7091
  }
5808
- this.#refreshTimer = setTimeout(() => {
5809
- this.#refreshTimer = null;
5810
- void this.#runProactiveRefresh();
5811
- }, Math.max(0, delayMs));
7092
+ this.#refreshTimer = setTimeout(
7093
+ () => {
7094
+ this.#refreshTimer = null;
7095
+ void this.#runProactiveRefresh();
7096
+ },
7097
+ Math.max(0, delayMs)
7098
+ );
5812
7099
  }
5813
7100
  /** (Re)arm the proactive-refresh timer against the current session's expiry. */
5814
7101
  #scheduleProactiveRefresh() {
@@ -5966,59 +7253,59 @@ function redact(value) {
5966
7253
  }
5967
7254
 
5968
7255
  // src/diagnostics/dump-format.ts
5969
- var import_zod3 = require("zod");
5970
- var DumpScalarSchema = import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number(), import_zod3.z.boolean()]);
5971
- var PropertyObservationSchema = import_zod3.z.object({
5972
- values: import_zod3.z.array(DumpScalarSchema),
5973
- unmapped: import_zod3.z.array(DumpScalarSchema),
5974
- enum: import_zod3.z.string().optional(),
5975
- count: import_zod3.z.number(),
5976
- firstSeen: import_zod3.z.number(),
5977
- lastSeen: import_zod3.z.number()
7256
+ var import_zod6 = require("zod");
7257
+ var DumpScalarSchema = import_zod6.z.union([import_zod6.z.string(), import_zod6.z.number(), import_zod6.z.boolean()]);
7258
+ var PropertyObservationSchema = import_zod6.z.object({
7259
+ values: import_zod6.z.array(DumpScalarSchema),
7260
+ unmapped: import_zod6.z.array(DumpScalarSchema),
7261
+ enum: import_zod6.z.string().optional(),
7262
+ count: import_zod6.z.number(),
7263
+ firstSeen: import_zod6.z.number(),
7264
+ lastSeen: import_zod6.z.number()
5978
7265
  });
5979
- var EventObservationSchema = import_zod3.z.object({
5980
- at: import_zod3.z.number(),
5981
- type: import_zod3.z.string(),
5982
- data: import_zod3.z.unknown().optional()
7266
+ var EventObservationSchema = import_zod6.z.object({
7267
+ at: import_zod6.z.number(),
7268
+ type: import_zod6.z.string(),
7269
+ data: import_zod6.z.unknown().optional()
5983
7270
  });
5984
- var RawFrameSchema = import_zod3.z.object({
5985
- at: import_zod3.z.number(),
5986
- source: import_zod3.z.string(),
5987
- payload: import_zod3.z.unknown()
7271
+ var RawFrameSchema = import_zod6.z.object({
7272
+ at: import_zod6.z.number(),
7273
+ source: import_zod6.z.string(),
7274
+ payload: import_zod6.z.unknown()
5988
7275
  });
5989
- var CommandSchema = import_zod3.z.object({
5990
- name: import_zod3.z.string(),
5991
- siid: import_zod3.z.number().optional(),
5992
- aiid: import_zod3.z.number().optional()
7276
+ var CommandSchema = import_zod6.z.object({
7277
+ name: import_zod6.z.string(),
7278
+ siid: import_zod6.z.number().optional(),
7279
+ aiid: import_zod6.z.number().optional()
5993
7280
  });
5994
- var SensorSchema = import_zod3.z.object({
5995
- model: import_zod3.z.string(),
5996
- channel: import_zod3.z.number().optional()
7281
+ var SensorSchema = import_zod6.z.object({
7282
+ model: import_zod6.z.string(),
7283
+ channel: import_zod6.z.number().optional()
5997
7284
  });
5998
- var DeviceDumpSchema = import_zod3.z.object({
5999
- schemaVersion: import_zod3.z.literal(1),
6000
- library: import_zod3.z.union([import_zod3.z.literal("nodedreame"), import_zod3.z.literal("nodewitt")]),
6001
- libraryVersion: import_zod3.z.string(),
6002
- device: import_zod3.z.object({
6003
- model: import_zod3.z.string(),
6004
- firmware: import_zod3.z.string().optional(),
6005
- region: import_zod3.z.string().optional(),
6006
- type: import_zod3.z.string().optional()
7285
+ var DeviceDumpSchema = import_zod6.z.object({
7286
+ schemaVersion: import_zod6.z.literal(1),
7287
+ library: import_zod6.z.union([import_zod6.z.literal("nodedreame"), import_zod6.z.literal("nodewitt")]),
7288
+ libraryVersion: import_zod6.z.string(),
7289
+ device: import_zod6.z.object({
7290
+ model: import_zod6.z.string(),
7291
+ firmware: import_zod6.z.string().optional(),
7292
+ region: import_zod6.z.string().optional(),
7293
+ type: import_zod6.z.string().optional()
6007
7294
  }),
6008
- observations: import_zod3.z.object({
6009
- properties: import_zod3.z.record(import_zod3.z.string(), PropertyObservationSchema),
6010
- events: import_zod3.z.array(EventObservationSchema),
6011
- rawFrames: import_zod3.z.array(RawFrameSchema).optional()
7295
+ observations: import_zod6.z.object({
7296
+ properties: import_zod6.z.record(import_zod6.z.string(), PropertyObservationSchema),
7297
+ events: import_zod6.z.array(EventObservationSchema),
7298
+ rawFrames: import_zod6.z.array(RawFrameSchema).optional()
6012
7299
  }),
6013
- catalog: import_zod3.z.object({
6014
- commands: import_zod3.z.array(CommandSchema).optional(),
6015
- capabilities: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).optional(),
6016
- sensors: import_zod3.z.array(SensorSchema).optional()
7300
+ catalog: import_zod6.z.object({
7301
+ commands: import_zod6.z.array(CommandSchema).optional(),
7302
+ capabilities: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()).optional(),
7303
+ sensors: import_zod6.z.array(SensorSchema).optional()
6017
7304
  }),
6018
- meta: import_zod3.z.object({
6019
- startedAt: import_zod3.z.number(),
6020
- durationMs: import_zod3.z.number(),
6021
- generatedAt: import_zod3.z.number()
7305
+ meta: import_zod6.z.object({
7306
+ startedAt: import_zod6.z.number(),
7307
+ durationMs: import_zod6.z.number(),
7308
+ generatedAt: import_zod6.z.number()
6022
7309
  })
6023
7310
  });
6024
7311
 
@@ -6307,21 +7594,939 @@ function createDumper(target, options) {
6307
7594
  function createClientDumper(client, options) {
6308
7595
  return client.devices.map((d) => createDumper(d, options));
6309
7596
  }
7597
+
7598
+ // src/video/monitor/detections.ts
7599
+ var asRecord = (value) => {
7600
+ if (typeof value === "string") {
7601
+ try {
7602
+ const parsed = JSON.parse(value);
7603
+ return parsed && typeof parsed === "object" ? parsed : null;
7604
+ } catch {
7605
+ return null;
7606
+ }
7607
+ }
7608
+ return value && typeof value === "object" ? value : null;
7609
+ };
7610
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
7611
+ var toMs = (raw) => {
7612
+ const n = num(raw);
7613
+ if (n === void 0) return Date.now();
7614
+ return n > 1e15 ? Math.round(n / 1e6) : n;
7615
+ };
7616
+ var toBox = (raw) => {
7617
+ if (Array.isArray(raw) && raw.length >= 4) {
7618
+ const [x2, y2, w2, h2] = raw;
7619
+ if ([x2, y2, w2, h2].every((v) => num(v) !== void 0)) {
7620
+ return { x: x2, y: y2, w: w2, h: h2 };
7621
+ }
7622
+ return null;
7623
+ }
7624
+ const r = asRecord(raw);
7625
+ if (!r) return null;
7626
+ const x = num(r["x"]);
7627
+ const y = num(r["y"]);
7628
+ const w = num(r["w"]) ?? num(r["width"]);
7629
+ const h = num(r["h"]) ?? num(r["height"]);
7630
+ if (x === void 0 || y === void 0 || w === void 0 || h === void 0) return null;
7631
+ const label = typeof r["label"] === "string" ? r["label"] : void 0;
7632
+ const score = num(r["score"]) ?? num(r["confidence"]);
7633
+ return { x, y, w, h, ...label !== void 0 ? { label } : {}, ...score !== void 0 ? { score } : {} };
7634
+ };
7635
+ function parsePersonFollow(value) {
7636
+ const r = asRecord(value);
7637
+ if (!r) return null;
7638
+ return {
7639
+ timestampMs: toMs(r["timestamp"] ?? r["time"]),
7640
+ box: toBox(r["bbox"] ?? r["box"])
7641
+ };
7642
+ }
7643
+ function parseObstacleData(value) {
7644
+ const r = asRecord(value);
7645
+ if (!r) return null;
7646
+ const list = r["boxlist"] ?? r["boxes"] ?? r["boxList"];
7647
+ const boxes = [];
7648
+ if (Array.isArray(list)) {
7649
+ for (const item of list) {
7650
+ const box = toBox(item);
7651
+ if (box) boxes.push(box);
7652
+ }
7653
+ }
7654
+ return { timestampMs: toMs(r["timestamp"] ?? r["time"]), boxes };
7655
+ }
7656
+
7657
+ // src/video/media/lv-rtmp-client.ts
7658
+ var import_node_net = __toESM(require("net"), 1);
7659
+ var import_node_events2 = require("events");
7660
+ var import_node_crypto6 = require("crypto");
7661
+
7662
+ // src/video/media/amf0.ts
7663
+ var AMF0 = {
7664
+ NUMBER: 0,
7665
+ BOOLEAN: 1,
7666
+ STRING: 2,
7667
+ OBJECT: 3,
7668
+ NULL: 5,
7669
+ UNDEFINED: 6,
7670
+ ECMA_ARRAY: 8,
7671
+ OBJECT_END: 9,
7672
+ STRICT_ARRAY: 10,
7673
+ LONG_STRING: 12
7674
+ };
7675
+ function amfNumber(n) {
7676
+ const b = Buffer.alloc(9);
7677
+ b[0] = AMF0.NUMBER;
7678
+ b.writeDoubleBE(n, 1);
7679
+ return b;
7680
+ }
7681
+ function amfBoolean(v) {
7682
+ return Buffer.from([AMF0.BOOLEAN, v ? 1 : 0]);
7683
+ }
7684
+ function amfString(s) {
7685
+ const str2 = Buffer.from(s, "utf8");
7686
+ const b = Buffer.alloc(3 + str2.length);
7687
+ b[0] = AMF0.STRING;
7688
+ b.writeUInt16BE(str2.length, 1);
7689
+ str2.copy(b, 3);
7690
+ return b;
7691
+ }
7692
+ function amfNull() {
7693
+ return Buffer.from([AMF0.NULL]);
7694
+ }
7695
+ function amfNamed(key2, value) {
7696
+ const k = Buffer.from(key2, "utf8");
7697
+ const b = Buffer.alloc(2 + k.length);
7698
+ b.writeUInt16BE(k.length, 0);
7699
+ k.copy(b, 2);
7700
+ return Buffer.concat([b, value]);
7701
+ }
7702
+ function amfObject(pairs) {
7703
+ const parts = [Buffer.from([AMF0.OBJECT])];
7704
+ for (const [k, v] of pairs) parts.push(amfNamed(k, v));
7705
+ parts.push(Buffer.from([0, 0, AMF0.OBJECT_END]));
7706
+ return Buffer.concat(parts);
7707
+ }
7708
+ function decodeOne(buf, pos) {
7709
+ const type = buf[pos];
7710
+ let p = pos + 1;
7711
+ switch (type) {
7712
+ case AMF0.NUMBER:
7713
+ return { value: buf.readDoubleBE(p), next: p + 8 };
7714
+ case AMF0.BOOLEAN:
7715
+ return { value: buf[p] !== 0, next: p + 1 };
7716
+ case AMF0.STRING: {
7717
+ const len = buf.readUInt16BE(p);
7718
+ return { value: buf.toString("utf8", p + 2, p + 2 + len), next: p + 2 + len };
7719
+ }
7720
+ case AMF0.LONG_STRING: {
7721
+ const len = buf.readUInt32BE(p);
7722
+ return { value: buf.toString("utf8", p + 4, p + 4 + len), next: p + 4 + len };
7723
+ }
7724
+ case AMF0.NULL:
7725
+ return { value: null, next: p };
7726
+ case AMF0.UNDEFINED:
7727
+ return { value: void 0, next: p };
7728
+ case AMF0.ECMA_ARRAY:
7729
+ case AMF0.OBJECT: {
7730
+ if (type === AMF0.ECMA_ARRAY) p += 4;
7731
+ const obj = {};
7732
+ for (; ; ) {
7733
+ const klen = buf.readUInt16BE(p);
7734
+ if (klen === 0 && buf[p + 2] === AMF0.OBJECT_END) return { value: obj, next: p + 3 };
7735
+ const key2 = buf.toString("utf8", p + 2, p + 2 + klen);
7736
+ const r = decodeOne(buf, p + 2 + klen);
7737
+ obj[key2] = r.value;
7738
+ p = r.next;
7739
+ }
7740
+ }
7741
+ case AMF0.STRICT_ARRAY: {
7742
+ const count = buf.readUInt32BE(p);
7743
+ p += 4;
7744
+ const arr = [];
7745
+ for (let i = 0; i < count; i++) {
7746
+ const r = decodeOne(buf, p);
7747
+ arr.push(r.value);
7748
+ p = r.next;
7749
+ }
7750
+ return { value: arr, next: p };
7751
+ }
7752
+ default:
7753
+ throw new Error(`AMF0: unsupported type 0x${(type ?? -1).toString(16)} at ${pos}`);
7754
+ }
7755
+ }
7756
+ function decodeAmf0(buf) {
7757
+ const out = [];
7758
+ let pos = 0;
7759
+ while (pos < buf.length) {
7760
+ const r = decodeOne(buf, pos);
7761
+ out.push(r.value);
7762
+ pos = r.next;
7763
+ }
7764
+ return out;
7765
+ }
7766
+
7767
+ // src/video/media/chunk.ts
7768
+ var MSG = {
7769
+ SET_CHUNK_SIZE: 1,
7770
+ ABORT: 2,
7771
+ ACK: 3,
7772
+ USER_CONTROL: 4,
7773
+ WINDOW_ACK_SIZE: 5,
7774
+ SET_PEER_BW: 6,
7775
+ AUDIO: 8,
7776
+ VIDEO: 9,
7777
+ DATA_AMF0: 18,
7778
+ COMMAND_AMF0: 20
7779
+ };
7780
+ var PRIVATE_MODE_CHUNK_SIZE = 6e4;
7781
+ function basicHeader(fmt, csid) {
7782
+ if (csid < 64) return Buffer.from([fmt << 6 | csid]);
7783
+ if (csid < 320) return Buffer.from([fmt << 6, csid - 64]);
7784
+ return Buffer.from([fmt << 6 | 1, csid - 64 & 255, csid - 64 >> 8]);
7785
+ }
7786
+ function encodeMessage(o) {
7787
+ if (o.body.length > PRIVATE_MODE_CHUNK_SIZE) {
7788
+ throw new Error(`message body ${o.body.length} exceeds private-mode chunk size ${PRIVATE_MODE_CHUNK_SIZE}`);
7789
+ }
7790
+ const ts = o.timestamp ?? 0;
7791
+ const hdrLen = o.fmt === 0 ? 11 : 7;
7792
+ const hdr = Buffer.alloc(hdrLen);
7793
+ hdr.writeUIntBE(ts >= 16777215 ? 16777215 : ts, 0, 3);
7794
+ hdr.writeUIntBE(o.body.length, 3, 3);
7795
+ hdr[6] = o.type;
7796
+ if (o.fmt === 0) hdr.writeUInt32LE(o.msid ?? 0, 7);
7797
+ const ext = ts >= 16777215 ? (() => {
7798
+ const e = Buffer.alloc(4);
7799
+ e.writeUInt32BE(ts, 0);
7800
+ return e;
7801
+ })() : Buffer.alloc(0);
7802
+ return Buffer.concat([basicHeader(o.fmt, o.csid), hdr, ext, o.body]);
7803
+ }
7804
+ function freshState() {
7805
+ return { timestamp: 0, delta: 0, length: 0, type: 0, msid: 0, extTs: false, partial: [], received: 0 };
7806
+ }
7807
+ var ChunkReader = class {
7808
+ chunkSize;
7809
+ pending = Buffer.alloc(0);
7810
+ streams = /* @__PURE__ */ new Map();
7811
+ constructor(chunkSize = PRIVATE_MODE_CHUNK_SIZE) {
7812
+ this.chunkSize = chunkSize;
7813
+ }
7814
+ feed(data) {
7815
+ this.pending = this.pending.length ? Buffer.concat([this.pending, data]) : data;
7816
+ const out = [];
7817
+ for (; ; ) {
7818
+ const consumed = this.tryReadChunk(out);
7819
+ if (consumed === 0) break;
7820
+ this.pending = this.pending.subarray(consumed);
7821
+ }
7822
+ return out;
7823
+ }
7824
+ /** Returns bytes consumed for one chunk, or 0 if more data is needed. */
7825
+ tryReadChunk(out) {
7826
+ const buf = this.pending;
7827
+ if (buf.length < 1) return 0;
7828
+ const fmt = buf[0] >> 6;
7829
+ let csid = buf[0] & 63;
7830
+ let p = 1;
7831
+ if (csid === 0) {
7832
+ if (buf.length < 2) return 0;
7833
+ csid = 64 + buf[1];
7834
+ p = 2;
7835
+ } else if (csid === 1) {
7836
+ if (buf.length < 3) return 0;
7837
+ csid = 64 + buf[1] + buf[2] * 256;
7838
+ p = 3;
7839
+ }
7840
+ const st = this.streams.get(csid) ?? freshState();
7841
+ const hdrLen = fmt === 0 ? 11 : fmt === 1 ? 7 : fmt === 2 ? 3 : 0;
7842
+ if (buf.length < p + hdrLen) return 0;
7843
+ let tsField = 0;
7844
+ if (fmt <= 2) {
7845
+ tsField = buf.readUIntBE(p, 3);
7846
+ if (fmt <= 1) {
7847
+ st.length = buf.readUIntBE(p + 3, 3);
7848
+ st.type = buf[p + 6];
7849
+ }
7850
+ if (fmt === 0) st.msid = buf.readUInt32LE(p + 7);
7851
+ st.extTs = tsField === 16777215;
7852
+ }
7853
+ p += hdrLen;
7854
+ if (st.extTs) {
7855
+ if (buf.length < p + 4) return 0;
7856
+ tsField = buf.readUInt32BE(p);
7857
+ p += 4;
7858
+ }
7859
+ const startOfMessage = st.received === 0;
7860
+ if (startOfMessage) {
7861
+ if (fmt === 0) {
7862
+ st.timestamp = tsField;
7863
+ st.delta = 0;
7864
+ } else if (fmt <= 2) {
7865
+ st.delta = tsField;
7866
+ st.timestamp += tsField;
7867
+ } else st.timestamp += st.delta;
7868
+ }
7869
+ const take = Math.min(this.chunkSize, st.length - st.received);
7870
+ if (buf.length < p + take) return 0;
7871
+ st.partial.push(Buffer.from(buf.subarray(p, p + take)));
7872
+ st.received += take;
7873
+ if (st.received >= st.length) {
7874
+ out.push({ csid, msid: st.msid, timestamp: st.timestamp, type: st.type, body: Buffer.concat(st.partial) });
7875
+ st.partial = [];
7876
+ st.received = 0;
7877
+ }
7878
+ this.streams.set(csid, st);
7879
+ return p + take;
7880
+ }
7881
+ };
7882
+
7883
+ // src/video/media/flv.ts
7884
+ var ANNEXB_START = Buffer.from([0, 0, 0, 1]);
7885
+ function parseVideoTag(body) {
7886
+ if (body.length < 5) return null;
7887
+ const b0 = body[0];
7888
+ return {
7889
+ frameType: b0 >> 4,
7890
+ codecId: b0 & 15,
7891
+ avcPacketType: body[1],
7892
+ compositionTime: body.readIntBE(2, 3),
7893
+ data: body.subarray(5)
7894
+ };
7895
+ }
7896
+ function parseAudioTag(body) {
7897
+ if (body.length < 1) return null;
7898
+ const b0 = body[0];
7899
+ const soundFormat = b0 >> 4;
7900
+ const isAac = soundFormat === 10;
7901
+ if (isAac && body.length < 2) return null;
7902
+ return {
7903
+ soundFormat,
7904
+ sampleRateIndex: b0 >> 2 & 3,
7905
+ channels: (b0 & 1) + 1,
7906
+ aacPacketType: isAac ? body[1] : null,
7907
+ data: body.subarray(isAac ? 2 : 1)
7908
+ };
7909
+ }
7910
+ function parseAvcConfig(rec) {
7911
+ const nalLengthSize = (rec[4] & 3) + 1;
7912
+ let p = 5;
7913
+ const sps = [];
7914
+ const pps = [];
7915
+ const numSps = rec[p++] & 31;
7916
+ for (let i = 0; i < numSps; i++) {
7917
+ const n = rec.readUInt16BE(p);
7918
+ sps.push(Buffer.from(rec.subarray(p + 2, p + 2 + n)));
7919
+ p += 2 + n;
7920
+ }
7921
+ const numPps = rec[p++];
7922
+ for (let i = 0; i < numPps; i++) {
7923
+ const n = rec.readUInt16BE(p);
7924
+ pps.push(Buffer.from(rec.subarray(p + 2, p + 2 + n)));
7925
+ p += 2 + n;
7926
+ }
7927
+ return { sps, pps, nalLengthSize };
7928
+ }
7929
+ function avccToAnnexB(data, cfg, prependParams) {
7930
+ const parts = [];
7931
+ if (prependParams) for (const nal of [...cfg.sps, ...cfg.pps]) parts.push(ANNEXB_START, nal);
7932
+ let p = 0;
7933
+ while (p + cfg.nalLengthSize <= data.length) {
7934
+ const len = data.readUIntBE(p, cfg.nalLengthSize);
7935
+ p += cfg.nalLengthSize;
7936
+ if (len === 0 || p + len > data.length) break;
7937
+ parts.push(ANNEXB_START, data.subarray(p, p + len));
7938
+ p += len;
7939
+ }
7940
+ return Buffer.concat(parts);
7941
+ }
7942
+ function parseAacConfig(asc) {
7943
+ const a0 = asc[0];
7944
+ const a1 = asc[1];
7945
+ return {
7946
+ objectType: a0 >> 3,
7947
+ samplingFrequencyIndex: (a0 & 7) << 1 | a1 >> 7,
7948
+ channelConfiguration: a1 >> 3 & 15
7949
+ };
7950
+ }
7951
+ var AAC_SAMPLE_RATES = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
7952
+ function aacSampleRate(cfg) {
7953
+ return AAC_SAMPLE_RATES[cfg.samplingFrequencyIndex] ?? 0;
7954
+ }
7955
+ function aacToAdts(raw, cfg) {
7956
+ const frameLen = raw.length + 7;
7957
+ const h = Buffer.alloc(7);
7958
+ h[0] = 255;
7959
+ h[1] = 241;
7960
+ h[2] = cfg.objectType - 1 << 6 | cfg.samplingFrequencyIndex << 2 | cfg.channelConfiguration >> 2;
7961
+ h[3] = (cfg.channelConfiguration & 3) << 6 | frameLen >> 11 & 3;
7962
+ h[4] = frameLen >> 3 & 255;
7963
+ h[5] = (frameLen & 7) << 5 | 31;
7964
+ h[6] = 252;
7965
+ return Buffer.concat([h, raw]);
7966
+ }
7967
+
7968
+ // src/video/media/lv-rtmp-client.ts
7969
+ var LV_STATUS = {
7970
+ REQUEST_AUDIO_TYPE: "NetStream.RequestAudioType",
7971
+ FORCE_IFRAME_START: "NetStream.ForceIFrame.Start",
7972
+ PING_START: "NetStream.Ping.Start",
7973
+ PING_RESPONSE: "NetStream.Ping.Response",
7974
+ PRE_CONNECT: "NetStream.PreConnect",
7975
+ KEEPALIVE: "NetStream.Keepalive",
7976
+ SERVER_INFO: "NetStream.ServerInfo",
7977
+ TRANSFER_DATA: "NetStream.TransferData",
7978
+ PLAY_START: "NetStream.Play.Start",
7979
+ /** Server signals the talk (intercom uplink) channel is ready. */
7980
+ TALK_READY: "NetStream.TalkReady"
7981
+ };
7982
+ var TALK_AUDIO_HEADER_G711A = 114;
7983
+ var CONNECT_CAPABILITIES = 15;
7984
+ var CONNECT_AUDIO_CODECS = 3191;
7985
+ var CONNECT_VIDEO_CODECS = 252;
7986
+ var CONNECT_VIDEO_FUNCTION = 1;
7987
+ var PLAY_START_LIVE = -1e3;
7988
+ var HANDSHAKE_C0_PRIVATE = 4;
7989
+ var HANDSHAKE_S0S1_LEN = 1537;
7990
+ var DEFAULT_PING_INTERVAL_MS = 3e4;
7991
+ var DEFAULT_CACHE_DURATION_MS = 3e3;
7992
+ var ACK_THRESHOLD_BYTES = 25e4;
7993
+ var CSID_CONTROL = 2;
7994
+ var CSID_COMMAND = 3;
7995
+ var CSID_TALK = 4;
7996
+ var CSID_PLAY = 8;
7997
+ var PLAY_STREAM_ID = 1;
7998
+ function parseRelayUrl(url, cacheDurationMs) {
7999
+ const m = /^rtmp:\/\/([^/:]+)(?::(\d+))?\/(.*)$/.exec(url);
8000
+ if (!m) throw new Error(`not an rtmp:// url: ${url}`);
8001
+ const host = m[1];
8002
+ const port = m[2] ? Number(m[2]) : 1935;
8003
+ const path = m[3];
8004
+ const slash = path.lastIndexOf("/");
8005
+ if (slash < 0) throw new Error(`relay url has no playpath: ${url}`);
8006
+ let app = path.slice(0, slash);
8007
+ const playpath = path.slice(slash + 1);
8008
+ if (!/[?&]cacheDuration=/.test(app)) app += `${app.includes("?") ? "&" : "?"}cacheDuration=${cacheDurationMs}`;
8009
+ return { host, port, app, playpath, tcUrl: `rtmp://${host}:${port}/${app}` };
8010
+ }
8011
+ var LvRtmpClient = class extends import_node_events2.EventEmitter {
8012
+ opts;
8013
+ endpoint;
8014
+ socket = null;
8015
+ handshakeBuf = Buffer.alloc(0);
8016
+ handshakeDone = false;
8017
+ c1Time = 0;
8018
+ reader = new ChunkReader(PRIVATE_MODE_CHUNK_SIZE);
8019
+ transactionId = 0;
8020
+ bytesIn = 0;
8021
+ bytesAcked = 0;
8022
+ ackWindow = ACK_THRESHOLD_BYTES;
8023
+ pingTimer = null;
8024
+ firstMessageSeen = false;
8025
+ avcConfig = null;
8026
+ aacConfig = null;
8027
+ closed = false;
8028
+ constructor(options) {
8029
+ super();
8030
+ this.opts = {
8031
+ url: options.url,
8032
+ cacheDurationMs: options.cacheDurationMs ?? DEFAULT_CACHE_DURATION_MS,
8033
+ requestAudioType: options.requestAudioType ?? true,
8034
+ ping: options.ping ?? true,
8035
+ pingIntervalMs: options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS,
8036
+ forceIFrameOnStart: options.forceIFrameOnStart ?? false,
8037
+ connectTimeoutMs: options.connectTimeoutMs ?? 1e4,
8038
+ log: options.log ?? (() => {
8039
+ })
8040
+ };
8041
+ this.endpoint = parseRelayUrl(this.opts.url, this.opts.cacheDurationMs);
8042
+ }
8043
+ /** Open TCP, write C0+C1 and the pipelined connect/play/RequestAudioType. Resolves once written. */
8044
+ connect() {
8045
+ return new Promise((resolve, reject) => {
8046
+ const sock = import_node_net.default.createConnection({ host: this.endpoint.host, port: this.endpoint.port });
8047
+ this.socket = sock;
8048
+ sock.setNoDelay(true);
8049
+ sock.setTimeout(this.opts.connectTimeoutMs, () => {
8050
+ if (!this.handshakeDone) this.fail(new Error("handshake timeout"));
8051
+ });
8052
+ sock.once("error", (err) => {
8053
+ this.emit("error", err);
8054
+ reject(err);
8055
+ });
8056
+ sock.on("data", (d) => this.onData(d));
8057
+ sock.on("close", () => this.onClose());
8058
+ sock.once("connect", () => {
8059
+ this.opts.log(`tcp connected ${this.endpoint.host}:${this.endpoint.port}`);
8060
+ sock.write(Buffer.concat([this.buildC0C1(), ...this.buildOpeningSequence()]));
8061
+ this.emit("connected");
8062
+ resolve();
8063
+ });
8064
+ });
8065
+ }
8066
+ close() {
8067
+ this.closed = true;
8068
+ if (this.pingTimer) clearInterval(this.pingTimer);
8069
+ this.pingTimer = null;
8070
+ this.socket?.destroy();
8071
+ }
8072
+ /** onStatus encoder — byte-exact replica of liblvmedia IOT_RTMP_SendMessage (csid 3, fmt 1, ts 0). */
8073
+ buildStatus(code, extra) {
8074
+ const pairs = [
8075
+ ["level", amfString("status")],
8076
+ ["code", amfString(code)],
8077
+ ["description", amfString("desc")]
8078
+ ];
8079
+ if (extra) pairs.push([extra[0], amfString(extra[1])]);
8080
+ const body = Buffer.concat([amfString("onStatus"), amfNumber(++this.transactionId), amfNull(), amfObject(pairs)]);
8081
+ return encodeMessage({ fmt: 1, csid: CSID_COMMAND, type: MSG.COMMAND_AMF0, body });
8082
+ }
8083
+ sendStatus(code, extra) {
8084
+ this.write(this.buildStatus(code, extra));
8085
+ this.opts.log(`>> onStatus ${code}${extra ? ` ${extra[0]}=${extra[1]}` : ""} (txn ${this.transactionId})`);
8086
+ }
8087
+ forceIFrame() {
8088
+ this.sendStatus(LV_STATUS.FORCE_IFRAME_START);
8089
+ }
8090
+ // ---------------------------------------------------------------- outgoing
8091
+ buildC0C1() {
8092
+ this.c1Time = Math.floor(performance.now()) >>> 0;
8093
+ const c1 = Buffer.alloc(1536);
8094
+ c1.writeUInt32BE(this.c1Time, 0);
8095
+ c1.writeUInt32BE(0, 4);
8096
+ (0, import_node_crypto6.randomBytes)(1528).copy(c1, 8);
8097
+ return Buffer.concat([Buffer.from([HANDSHAKE_C0_PRIVATE]), c1]);
8098
+ }
8099
+ buildConnect() {
8100
+ const obj = amfObject([
8101
+ ["app", amfString(this.endpoint.app)],
8102
+ ["tcUrl", amfString(this.endpoint.tcUrl)],
8103
+ ["fpad", amfBoolean(false)],
8104
+ ["capabilities", amfNumber(CONNECT_CAPABILITIES)],
8105
+ ["audioCodecs", amfNumber(CONNECT_AUDIO_CODECS)],
8106
+ ["videoCodecs", amfNumber(CONNECT_VIDEO_CODECS)],
8107
+ ["videoFunction", amfNumber(CONNECT_VIDEO_FUNCTION)]
8108
+ ]);
8109
+ const body = Buffer.concat([amfString("connect"), amfNumber(++this.transactionId), obj]);
8110
+ return encodeMessage({ fmt: 0, csid: CSID_COMMAND, msid: 0, type: MSG.COMMAND_AMF0, body });
8111
+ }
8112
+ buildPlay() {
8113
+ const body = Buffer.concat([
8114
+ amfString("play"),
8115
+ amfNumber(++this.transactionId),
8116
+ amfNull(),
8117
+ amfString(this.endpoint.playpath),
8118
+ amfNumber(PLAY_START_LIVE)
8119
+ ]);
8120
+ return encodeMessage({ fmt: 0, csid: CSID_PLAY, msid: PLAY_STREAM_ID, type: MSG.COMMAND_AMF0, body });
8121
+ }
8122
+ /** Exposed for the byte-exact self-test against the pcap. */
8123
+ buildOpeningSequence() {
8124
+ const seq = [this.buildConnect(), this.buildPlay()];
8125
+ if (this.opts.requestAudioType) seq.push(this.buildStatus(LV_STATUS.REQUEST_AUDIO_TYPE, ["audio_type", "1"]));
8126
+ return seq;
8127
+ }
8128
+ /**
8129
+ * Push one intercom (talk-back) audio frame UPSTREAM to the device — an RTMP
8130
+ * type-8 audio message on the talk chunk stream (csid 4, msid 1), exactly as
8131
+ * liblvmedia's `IOT_RTMP_SendAudio` does. `payload` is the raw codec audio
8132
+ * (default framing = G.711 A-law); `tsMs` is a running millisecond timestamp.
8133
+ * Only meaningful after the server sends {@link LV_STATUS.TALK_READY}.
8134
+ */
8135
+ sendAudioFrame(payload, tsMs, header = TALK_AUDIO_HEADER_G711A) {
8136
+ const body = Buffer.concat([Buffer.from([header]), payload]);
8137
+ this.write(
8138
+ encodeMessage({ fmt: 0, csid: CSID_TALK, msid: PLAY_STREAM_ID, timestamp: tsMs >>> 0, type: MSG.AUDIO, body })
8139
+ );
8140
+ }
8141
+ write(buf) {
8142
+ if (!this.socket || this.socket.destroyed) return;
8143
+ this.socket.write(buf);
8144
+ }
8145
+ sendAck() {
8146
+ const b = Buffer.alloc(4);
8147
+ b.writeUInt32BE(this.bytesIn >>> 0, 0);
8148
+ this.write(encodeMessage({ fmt: 0, csid: CSID_CONTROL, msid: 0, type: MSG.ACK, body: b }));
8149
+ this.bytesAcked = this.bytesIn;
8150
+ }
8151
+ sendPingResponse(serverTs) {
8152
+ const body = Buffer.concat([Buffer.from([0, 7]), serverTs]);
8153
+ this.write(encodeMessage({ fmt: 0, csid: CSID_CONTROL, msid: 0, type: MSG.USER_CONTROL, body }));
8154
+ }
8155
+ startPinging() {
8156
+ if (!this.opts.ping || this.pingTimer) return;
8157
+ this.sendStatus(LV_STATUS.PING_START);
8158
+ this.pingTimer = setInterval(() => this.sendStatus(LV_STATUS.PING_START), this.opts.pingIntervalMs);
8159
+ }
8160
+ // ---------------------------------------------------------------- incoming
8161
+ onData(data) {
8162
+ this.bytesIn += data.length;
8163
+ let payload = data;
8164
+ if (!this.handshakeDone) {
8165
+ this.handshakeBuf = Buffer.concat([this.handshakeBuf, data]);
8166
+ if (this.handshakeBuf.length < HANDSHAKE_S0S1_LEN) return;
8167
+ this.onHandshake(this.handshakeBuf.subarray(0, HANDSHAKE_S0S1_LEN));
8168
+ payload = this.handshakeBuf.subarray(HANDSHAKE_S0S1_LEN);
8169
+ this.handshakeBuf = Buffer.alloc(0);
8170
+ this.handshakeDone = true;
8171
+ if (payload.length === 0) return;
8172
+ }
8173
+ let messages;
8174
+ try {
8175
+ messages = this.reader.feed(payload);
8176
+ } catch (err) {
8177
+ this.fail(err instanceof Error ? err : new Error(String(err)));
8178
+ return;
8179
+ }
8180
+ for (const m of messages) this.dispatch(m);
8181
+ if (this.bytesIn - this.bytesAcked >= this.ackWindow) this.sendAck();
8182
+ }
8183
+ onHandshake(s0s1) {
8184
+ const s0 = s0s1[0];
8185
+ const serverTime = s0s1.readUInt32BE(1);
8186
+ const zero = s0s1.readUInt32BE(5);
8187
+ 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)`);
8188
+ }
8189
+ dispatch(m) {
8190
+ if (!this.firstMessageSeen) {
8191
+ this.firstMessageSeen = true;
8192
+ this.startPinging();
8193
+ }
8194
+ switch (m.type) {
8195
+ case MSG.SET_CHUNK_SIZE: {
8196
+ const size = m.body.readUInt32BE(0) & 2147483647;
8197
+ this.opts.log(`<< SetChunkSize ${size}`);
8198
+ this.reader.chunkSize = size;
8199
+ return;
8200
+ }
8201
+ case MSG.WINDOW_ACK_SIZE:
8202
+ this.ackWindow = Math.min(ACK_THRESHOLD_BYTES, m.body.readUInt32BE(0));
8203
+ this.opts.log(`<< WindowAckSize ${m.body.readUInt32BE(0)}`);
8204
+ return;
8205
+ case MSG.SET_PEER_BW:
8206
+ this.opts.log(`<< SetPeerBandwidth ${m.body.readUInt32BE(0)} limit=${m.body[4]}`);
8207
+ return;
8208
+ case MSG.ACK:
8209
+ case MSG.ABORT:
8210
+ return;
8211
+ case MSG.USER_CONTROL:
8212
+ return this.onUserControl(m.body);
8213
+ case MSG.COMMAND_AMF0:
8214
+ return this.onCommand(m);
8215
+ case MSG.DATA_AMF0:
8216
+ return this.onDataMessage(m.body);
8217
+ case MSG.AUDIO:
8218
+ return this.onAudio(m);
8219
+ case MSG.VIDEO:
8220
+ return this.onVideo(m);
8221
+ default:
8222
+ this.opts.log(`<< unknown message type ${m.type} len=${m.body.length}`);
8223
+ }
8224
+ }
8225
+ onUserControl(body) {
8226
+ const event = body.readUInt16BE(0);
8227
+ const names = { 0: "StreamBegin", 1: "StreamEOF", 2: "StreamDry", 4: "StreamIsRecorded", 6: "PingRequest", 7: "PingResponse" };
8228
+ this.opts.log(`<< UserControl ${names[event] ?? event} ${body.subarray(2).toString("hex")}`);
8229
+ if (event === 6) this.sendPingResponse(body.subarray(2, 6));
8230
+ }
8231
+ onCommand(m) {
8232
+ let values;
8233
+ try {
8234
+ values = decodeAmf0(m.body);
8235
+ } catch (err) {
8236
+ this.opts.log(`<< undecodable command: ${m.body.toString("hex")}`);
8237
+ return;
8238
+ }
8239
+ const [name, txn, , info] = values;
8240
+ this.opts.log(`<< ${String(name)} ${JSON.stringify(values.slice(1))}`);
8241
+ this.emit("command", String(name), values);
8242
+ if (name === "onStatus" && info && typeof info === "object" && !Array.isArray(info)) {
8243
+ const fields = info;
8244
+ this.emit("status", {
8245
+ code: String(fields.code ?? ""),
8246
+ level: String(fields.level ?? ""),
8247
+ description: String(fields.description ?? ""),
8248
+ fields,
8249
+ transactionId: typeof txn === "number" ? txn : 0
8250
+ });
8251
+ if (fields.code === LV_STATUS.PLAY_START && this.opts.forceIFrameOnStart) this.forceIFrame();
8252
+ }
8253
+ }
8254
+ onDataMessage(body) {
8255
+ try {
8256
+ const values = decodeAmf0(body);
8257
+ this.opts.log(`<< data ${JSON.stringify(values).slice(0, 300)}`);
8258
+ const obj = values.find((v) => v && typeof v === "object" && !Array.isArray(v));
8259
+ if (obj) this.emit("metadata", obj);
8260
+ } catch {
8261
+ this.opts.log(`<< undecodable data message len=${body.length}`);
8262
+ }
8263
+ }
8264
+ onAudio(m) {
8265
+ this.emit("audio", { timestamp: m.timestamp, body: m.body });
8266
+ const tag = parseAudioTag(m.body);
8267
+ if (!tag) return;
8268
+ if (tag.soundFormat === 10) {
8269
+ if (tag.aacPacketType === 0) {
8270
+ this.aacConfig = parseAacConfig(tag.data);
8271
+ this.emit("audioInfo", { codec: "aac", sampleRate: aacSampleRate(this.aacConfig), channels: this.aacConfig.channelConfiguration });
8272
+ return;
8273
+ }
8274
+ if (this.aacConfig) this.emit("audioFrame", aacToAdts(tag.data, this.aacConfig));
8275
+ return;
8276
+ }
8277
+ if (tag.soundFormat === 7 || tag.soundFormat === 8) {
8278
+ if (!this.aacConfig) this.emit("audioInfo", { codec: tag.soundFormat === 7 ? "g711a" : "g711u", sampleRate: 8e3, channels: 1 });
8279
+ this.emit("audioFrame", tag.data);
8280
+ return;
8281
+ }
8282
+ this.emit("audioInfo", { codec: "unknown", sampleRate: 0, channels: tag.channels });
8283
+ }
8284
+ onVideo(m) {
8285
+ this.emit("video", { timestamp: m.timestamp, body: m.body });
8286
+ const tag = parseVideoTag(m.body);
8287
+ if (!tag || tag.codecId !== 7) return;
8288
+ if (tag.avcPacketType === 0) {
8289
+ this.avcConfig = parseAvcConfig(tag.data);
8290
+ return;
8291
+ }
8292
+ if (tag.avcPacketType !== 1 || !this.avcConfig) return;
8293
+ const isKeyframe = tag.frameType === 1;
8294
+ this.emit("videoAccessUnit", {
8295
+ data: avccToAnnexB(tag.data, this.avcConfig, isKeyframe),
8296
+ isKeyframe,
8297
+ videoType: "H264",
8298
+ microseconds: m.timestamp * 1e3,
8299
+ time: m.timestamp + tag.compositionTime
8300
+ });
8301
+ }
8302
+ fail(err) {
8303
+ this.opts.log(`!! ${err.message}`);
8304
+ this.emit("error", err);
8305
+ this.close();
8306
+ }
8307
+ onClose() {
8308
+ if (this.pingTimer) clearInterval(this.pingTimer);
8309
+ this.pingTimer = null;
8310
+ if (!this.closed) this.opts.log("socket closed by peer");
8311
+ this.closed = true;
8312
+ this.emit("close");
8313
+ }
8314
+ };
8315
+
8316
+ // src/video/media/g711.ts
8317
+ var ALAW_SEG_END = [31, 63, 127, 255, 511, 1023, 2047, 4095];
8318
+ function pcm16ToALaw(sample) {
8319
+ let s = sample;
8320
+ let sign = ~s >> 8 & 128;
8321
+ if (sign === 0) s = -s;
8322
+ if (s > 32635) s = 32635;
8323
+ let aval;
8324
+ if (s >= 256) {
8325
+ let seg = 7;
8326
+ for (let i = 0; i < 8; i++) {
8327
+ if (s <= ALAW_SEG_END[i]) {
8328
+ seg = i;
8329
+ break;
8330
+ }
8331
+ }
8332
+ const mantissa = s >> seg + 3 & 15;
8333
+ aval = seg << 4 | mantissa;
8334
+ } else {
8335
+ aval = s >> 4;
8336
+ }
8337
+ return (aval ^ sign ^ 85) & 255;
8338
+ }
8339
+ function pcm16leToALaw(pcm) {
8340
+ const count = pcm.length >> 1;
8341
+ const out = Buffer.allocUnsafe(count);
8342
+ for (let i = 0; i < count; i++) {
8343
+ out[i] = pcm16ToALaw(pcm.readInt16LE(i * 2));
8344
+ }
8345
+ return out;
8346
+ }
8347
+
8348
+ // src/video/media/camera-stream.ts
8349
+ var DreameCameraStream = class extends TypedEmitter {
8350
+ #controller;
8351
+ #clientFactory;
8352
+ #log;
8353
+ #client = null;
8354
+ #state = "idle";
8355
+ #videoFrames = 0;
8356
+ #audioFrames = 0;
8357
+ #talkReady = false;
8358
+ #talkWaiters = [];
8359
+ #talkTs = 0;
8360
+ constructor(input) {
8361
+ super();
8362
+ this.#controller = input.controller;
8363
+ this.#log = input.log ?? (() => {
8364
+ });
8365
+ this.#clientFactory = input.clientFactory ?? ((url) => new LvRtmpClient({
8366
+ url,
8367
+ ...input.requestAudioType !== void 0 ? { requestAudioType: input.requestAudioType } : {},
8368
+ ...input.ping !== void 0 ? { ping: input.ping } : {},
8369
+ ...input.log ? { log: input.log } : {}
8370
+ }));
8371
+ }
8372
+ /** True while frames are flowing. */
8373
+ get isRunning() {
8374
+ return this.#state === "running";
8375
+ }
8376
+ /**
8377
+ * Cold-start the monitor, connect to the relay, and begin emitting frames.
8378
+ * Resolves once connected. Throws (and cleans up) if the monitor or relay
8379
+ * connection fails.
8380
+ */
8381
+ async start() {
8382
+ if (this.#state !== "idle") {
8383
+ throw new DreameError(`camera stream cannot start from state '${this.#state}'`);
8384
+ }
8385
+ this.#state = "starting";
8386
+ try {
8387
+ this.#log("[nodedreame:stream] starting");
8388
+ const handle = await this.#controller.open();
8389
+ const client = this.#clientFactory(handle.rtmpUrl);
8390
+ this.#client = client;
8391
+ client.on("videoAccessUnit", (e) => {
8392
+ this.#videoFrames += 1;
8393
+ if (this.#videoFrames === 1) this.#log("[nodedreame:stream] first video access unit");
8394
+ this.emit("videoAccessUnit", e);
8395
+ });
8396
+ client.on("audioFrame", (buf) => {
8397
+ this.#audioFrames += 1;
8398
+ if (this.#audioFrames === 1) this.#log("[nodedreame:stream] first audio frame");
8399
+ this.emit("audioFrame", buf);
8400
+ });
8401
+ client.on("audioInfo", (info) => {
8402
+ this.#log(`[nodedreame:stream] audio ${info.codec} ${info.sampleRate}Hz x${info.channels}`);
8403
+ this.emit("audioInfo", info);
8404
+ });
8405
+ client.on("status", (e) => {
8406
+ if (e.code === LV_STATUS.TALK_READY) {
8407
+ this.#log("[nodedreame:stream] talk channel ready");
8408
+ this.#talkReady = true;
8409
+ const waiters = this.#talkWaiters;
8410
+ this.#talkWaiters = [];
8411
+ for (const w of waiters) w();
8412
+ }
8413
+ });
8414
+ client.on("error", (err) => {
8415
+ this.#log(`[nodedreame:stream] rtmp error: ${err.message}`);
8416
+ this.emit("error", err);
8417
+ });
8418
+ client.on("close", () => {
8419
+ this.#log("[nodedreame:stream] upstream relay closed");
8420
+ if (this.#state === "running") void this.stop();
8421
+ });
8422
+ await client.connect();
8423
+ this.#state = "running";
8424
+ this.#log("[nodedreame:stream] connected to relay");
8425
+ this.emit("started", { rtmpUrl: handle.rtmpUrl });
8426
+ return { rtmpUrl: handle.rtmpUrl };
8427
+ } catch (err) {
8428
+ this.#state = "idle";
8429
+ await this.#safeTeardown();
8430
+ throw err;
8431
+ }
8432
+ }
8433
+ /** Close the relay connection and release the monitor. Idempotent. */
8434
+ async stop() {
8435
+ if (this.#state === "idle" || this.#state === "stopping") return;
8436
+ this.#state = "stopping";
8437
+ this.#log(`[nodedreame:stream] stopping (video=${this.#videoFrames} audio=${this.#audioFrames})`);
8438
+ await this.#safeTeardown();
8439
+ this.#state = "idle";
8440
+ this.emit("stopped");
8441
+ }
8442
+ /**
8443
+ * Open the two-way intercom: send the MIoT start, then resolve once the relay
8444
+ * signals the talk channel is ready (so it's safe to {@link sendTalkPcm}).
8445
+ * Rejects if not ready within `timeoutMs`.
8446
+ */
8447
+ async startTalk(opts) {
8448
+ if (this.#state !== "running") throw new DreameError("stream is not running; start() first");
8449
+ this.#log("[nodedreame:stream] intercom start");
8450
+ await this.#controller.startIntercom(
8451
+ opts?.needRecordSound !== void 0 || opts?.videoCall !== void 0 ? { ...opts.needRecordSound !== void 0 ? { needRecordSound: opts.needRecordSound } : {}, ...opts.videoCall ? { videoCall: true } : {} } : void 0
8452
+ );
8453
+ this.#talkTs = 0;
8454
+ await this.whenTalkReady(opts?.timeoutMs ?? 5e3);
8455
+ }
8456
+ /** Resolve when the talk channel is ready (or reject on timeout). */
8457
+ whenTalkReady(timeoutMs = 5e3) {
8458
+ if (this.#talkReady) return Promise.resolve();
8459
+ return new Promise((resolve, reject) => {
8460
+ const timer = setTimeout(() => reject(new DreameError("timed out waiting for TalkReady")), timeoutMs);
8461
+ timer.unref?.();
8462
+ this.#talkWaiters.push(() => {
8463
+ clearTimeout(timer);
8464
+ resolve();
8465
+ });
8466
+ });
8467
+ }
8468
+ /**
8469
+ * Encode 16-bit LE mono PCM (8 kHz) to G.711 A-law and push it upstream.
8470
+ * `tsMs` defaults to a running clock advanced by the frame duration.
8471
+ */
8472
+ sendTalkPcm(pcm16le, tsMs) {
8473
+ const alaw = pcm16leToALaw(pcm16le);
8474
+ this.sendTalkAlaw(alaw, tsMs);
8475
+ }
8476
+ /** Push raw A-law audio upstream (advancing the running talk clock). */
8477
+ sendTalkAlaw(alaw, tsMs) {
8478
+ if (!this.#client) throw new DreameError("stream is not running");
8479
+ const ts = tsMs ?? this.#talkTs;
8480
+ this.#client.sendAudioFrame(alaw, ts);
8481
+ this.#talkTs = ts + Math.round(alaw.length / 8);
8482
+ }
8483
+ /** Close the two-way intercom (MIoT stop). */
8484
+ async stopTalk() {
8485
+ this.#talkReady = false;
8486
+ this.#log("[nodedreame:stream] intercom stop");
8487
+ await this.#controller.stopIntercom();
8488
+ }
8489
+ async #safeTeardown() {
8490
+ this.#talkReady = false;
8491
+ this.#talkWaiters = [];
8492
+ try {
8493
+ this.#client?.close();
8494
+ } catch {
8495
+ }
8496
+ this.#client = null;
8497
+ try {
8498
+ await this.#controller.close();
8499
+ } catch (err) {
8500
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
8501
+ }
8502
+ }
8503
+ };
6310
8504
  // Annotate the CommonJS export names for ESM import in node:
6311
8505
  0 && (module.exports = {
6312
8506
  AI_FEATURE_BIT,
6313
8507
  AI_FEATURE_JSON_KEY,
8508
+ ALIYUN_REGION_ID,
6314
8509
  AUTO_SWITCH_JSON_KEY,
6315
8510
  BaseDevice,
6316
8511
  ChargingStatus,
6317
8512
  CleaningMode,
8513
+ DRIVE_DIRECTIONS,
6318
8514
  DefaultCapabilityResolver,
6319
8515
  DreameApiError,
6320
8516
  DreameAuthError,
8517
+ DreameCameraController,
8518
+ DreameCameraStream,
6321
8519
  DreameDeviceOfflineError,
6322
8520
  DreameError,
6323
8521
  DreameTransportError,
8522
+ DreameVideoSession,
6324
8523
  LIBRARY_NAME,
8524
+ LV_STATUS,
8525
+ LvRtmpClient,
8526
+ MONITOR_AIID,
8527
+ MONITOR_PIID,
8528
+ MONITOR_SIID,
8529
+ MONITOR_VENDOR_TOKEN,
6325
8530
  MOWER_MODEL_CAPABILITIES,
6326
8531
  MOWER_TASK_SUBSTATES,
6327
8532
  MiotError,
@@ -6334,34 +8539,66 @@ function createClientDumper(client, options) {
6334
8539
  MowerStatus,
6335
8540
  MowerTaskStatus,
6336
8541
  Nodreame,
8542
+ PET_SOUNDS,
6337
8543
  SuctionLevel,
8544
+ TALK_AUDIO_HEADER_G711A,
6338
8545
  TaskStatus,
8546
+ VACUUM_ACTIONS,
6339
8547
  VACUUM_CONSUMABLES,
6340
8548
  VACUUM_MODEL_CAPABILITIES,
8549
+ VACUUM_MOVE,
8550
+ VACUUM_SIID,
6341
8551
  VacuumCapabilityResolver,
6342
8552
  VacuumDevice,
6343
8553
  WaterVolume,
8554
+ aacSampleRate,
8555
+ aacToAdts,
8556
+ avccToAnnexB,
6344
8557
  consumableSpec,
6345
8558
  createClientDumper,
6346
8559
  createDumper,
8560
+ createIotSession,
6347
8561
  decodeAiFeature,
6348
8562
  decodeAutoSwitch,
6349
8563
  decodeAutoSwitchAll,
8564
+ decodeWifiSignalMap,
6350
8565
  encodeAiFeatureWrite,
6351
8566
  encodeAutoSwitchWrite,
8567
+ explainNoCameraChannel,
6352
8568
  extractMowerConsumableValues,
8569
+ getAliyunAuthCode,
8570
+ getDeviceVideoProfile,
6353
8571
  getMowerCapabilities,
6354
8572
  getVacuumCapabilities,
8573
+ getVideoAccessToken,
8574
+ getVideoFamilyId,
8575
+ hashAccessCode,
6355
8576
  isAuthRefusedError,
6356
8577
  isDreameConsumableKey,
8578
+ listDevices,
8579
+ loginByOauth,
8580
+ makeMonitorSession,
6357
8581
  mowerConsumableIndex,
6358
8582
  mowerFaultSeverity,
8583
+ parseAacConfig,
8584
+ parseAvcConfig,
8585
+ parseConnectivity,
6359
8586
  parseMowerConsumables,
6360
8587
  parseMowerHeartbeat,
6361
8588
  parseMowingProgress,
8589
+ parseObstacleData,
8590
+ parsePersonFollow,
8591
+ parseRelayUrl,
8592
+ pcm16ToALaw,
8593
+ pcm16leToALaw,
8594
+ remoteDriveValue,
6362
8595
  renderMowerSvg,
6363
8596
  renderVacuumPng,
8597
+ renderWifiSignalPng,
6364
8598
  resolveCapabilities,
6365
- supportedAutoSwitchKeys
8599
+ signApiGatewayRequest,
8600
+ streamQuery,
8601
+ supportedAutoSwitchKeys,
8602
+ toVideoProfile
6366
8603
  });
6367
8604
  //# sourceMappingURL=index.cjs.map