@camstack/addon-provider-dreame 0.2.92 → 0.2.93

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/addon.js CHANGED
@@ -144141,9 +144141,28 @@ var { mulaw, alaw } = resolveAlawmulaw();
144141
144141
  * name, and WE own the TCP socket — when the broker's reader detaches we know
144142
144142
  * the viewer is gone and release the monitor at once, instead of polling the
144143
144143
  * broker for consumers or waiting on a duration cap.
144144
+ *
144145
+ * And we own the FEED: the relay dies in three ways nodedreame reports and one
144146
+ * it cannot — the socket errors (`error`; with no listener an EventEmitter
144147
+ * throws, and on 2026-09-09 a `read ETIMEDOUT` took the whole provider down),
144148
+ * the relay closes and the stream stops itself (`stopped`), or the socket goes
144149
+ * silent for minutes before the kernel gives up (nothing at all: 16 min of a
144150
+ * broker dialling a port that accepted and sent nothing). The server reports
144151
+ * every one of them ONCE through `onFeedLost`, and never its own close, so the
144152
+ * device can put the lazy sentinel back and let the next dial be a wake (D424).
144144
144153
  */
144145
144154
  var VIDEO_PT = 96;
144146
144155
  var AUDIO_PT = 97;
144156
+ /**
144157
+ * How long the feed may go without a video access unit before the relay is
144158
+ * declared dead. The broker gives a source 8 s (`RTP_ACTIVITY_TIMEOUT_MS`);
144159
+ * this sits just past it so a stall is judged by the side that can act on it.
144160
+ */
144161
+ var FEED_STALL_MS = 1e4;
144162
+ var FEED_STALL_SWEEP_MS = 1e3;
144163
+ function describeLoss(loss) {
144164
+ return loss.error === null ? loss.reason : `${loss.reason}: ${loss.error.message}`;
144165
+ }
144147
144166
  function deferred() {
144148
144167
  let resolve = () => void 0;
144149
144168
  return {
@@ -144176,11 +144195,33 @@ function consoleInto(logger) {
144176
144195
  * server calls `onConsumerDisconnect` when the last reader detaches.
144177
144196
  */
144178
144197
  async function startRfc4571Server(params) {
144179
- const { stream, logger, onConsumerDisconnect, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3 } = params;
144198
+ const { stream, logger, onConsumerDisconnect, onFeedLost, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3, stallTimeoutMs = FEED_STALL_MS } = params;
144180
144199
  const videoRef = { current: null };
144181
144200
  const audioRef = { current: null };
144182
144201
  const firstKeyframe = deferred();
144183
144202
  const firstAudio = deferred();
144203
+ const firstLoss = deferred();
144204
+ let pendingLoss = null;
144205
+ let toreDown = false;
144206
+ let serving = false;
144207
+ let lastVideoAt = 0;
144208
+ const reportLoss = (loss) => {
144209
+ if (toreDown || pendingLoss !== null) return;
144210
+ pendingLoss = loss;
144211
+ firstLoss.resolve(loss);
144212
+ if (serving) onFeedLost(loss);
144213
+ };
144214
+ stream.on("error", (err) => reportLoss({
144215
+ reason: "error",
144216
+ error: err
144217
+ }));
144218
+ stream.on("stopped", () => reportLoss({
144219
+ reason: "stopped",
144220
+ error: null
144221
+ }));
144222
+ stream.on("videoAccessUnit", () => {
144223
+ lastVideoAt = Date.now();
144224
+ });
144184
144225
  stream.on("videoAccessUnit", (e) => {
144185
144226
  if (videoRef.current !== null) return;
144186
144227
  const p = extractH264ParamSetsFromAccessUnit(e.data);
@@ -144206,8 +144247,11 @@ async function startRfc4571Server(params) {
144206
144247
  }
144207
144248
  });
144208
144249
  await stream.start();
144250
+ const lostBeforeServing = firstLoss.promise.then((loss) => {
144251
+ throw new Error(`relay lost before the first keyframe (${describeLoss(loss)})`);
144252
+ });
144209
144253
  try {
144210
- await withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe");
144254
+ await Promise.race([withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe"), lostBeforeServing]);
144211
144255
  } catch (err) {
144212
144256
  await stream.stop().catch(() => void 0);
144213
144257
  throw err;
@@ -144215,6 +144259,10 @@ async function startRfc4571Server(params) {
144215
144259
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144216
144260
  setTimeout(resolve, audioGraceMs).unref?.();
144217
144261
  })]);
144262
+ if (pendingLoss !== null) {
144263
+ await stream.stop().catch(() => void 0);
144264
+ throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
144265
+ }
144218
144266
  const paramSets = videoRef.current;
144219
144267
  if (paramSets === null) throw new Error("no parameter sets after the first keyframe");
144220
144268
  const audioFacts = audioRef.current;
@@ -144234,7 +144282,6 @@ async function startRfc4571Server(params) {
144234
144282
  if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144235
144283
  let liveClients = 0;
144236
144284
  let idleTimer = null;
144237
- let toreDown = false;
144238
144285
  const server = node_net.default.createServer((socket) => {
144239
144286
  liveClients += 1;
144240
144287
  if (idleTimer !== null) clearTimeout(idleTimer);
@@ -144261,6 +144308,15 @@ async function startRfc4571Server(params) {
144261
144308
  });
144262
144309
  const addr = server.address();
144263
144310
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144311
+ const stallSweep = setInterval(() => {
144312
+ if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144313
+ reportLoss({
144314
+ reason: "stalled",
144315
+ error: null
144316
+ });
144317
+ }, Math.max(5, Math.min(FEED_STALL_SWEEP_MS, Math.floor(stallTimeoutMs / 2))));
144318
+ stallSweep.unref?.();
144319
+ serving = true;
144264
144320
  return {
144265
144321
  host: bindHost,
144266
144322
  port: addr.port,
@@ -144272,6 +144328,7 @@ async function startRfc4571Server(params) {
144272
144328
  } : null,
144273
144329
  close: async () => {
144274
144330
  toreDown = true;
144331
+ clearInterval(stallSweep);
144275
144332
  if (idleTimer !== null) clearTimeout(idleTimer);
144276
144333
  try {
144277
144334
  muxer.close();
@@ -144551,6 +144608,9 @@ var DreameCameraDevice = class extends DreameChildDevice {
144551
144608
  keyframeTimeoutMs: WAKE_TIMEOUT_MS,
144552
144609
  onConsumerDisconnect: () => {
144553
144610
  this.closeStream("idle");
144611
+ },
144612
+ onFeedLost: (loss) => {
144613
+ this.onFeedLost(loss);
144554
144614
  }
144555
144615
  });
144556
144616
  this.live = {
@@ -144590,6 +144650,22 @@ var DreameCameraDevice = class extends DreameChildDevice {
144590
144650
  });
144591
144651
  return this.opening;
144592
144652
  }
144653
+ /**
144654
+ * The feed died under an open monitor. The session is closed — the lazy
144655
+ * sentinel goes back on the broker — so the broker's next dial asks for a
144656
+ * materialize, which is the wake. Logged: this branch drops a live stream.
144657
+ */
144658
+ onFeedLost(loss) {
144659
+ this.ctx.logger.warn("robot camera: relay feed lost — closing the monitor; the next dial re-opens it", {
144660
+ tags: { deviceId: this.id },
144661
+ meta: {
144662
+ reason: loss.reason,
144663
+ error: loss.error === null ? null : errMsg(loss.error),
144664
+ openMs: Date.now() - this.sessionOpenedAt
144665
+ }
144666
+ });
144667
+ this.closeStream("feed-lost");
144668
+ }
144593
144669
  armMaxDuration() {
144594
144670
  if (this.maxTimer !== null) clearTimeout(this.maxTimer);
144595
144671
  this.maxTimer = setTimeout(() => {
package/dist/addon.mjs CHANGED
@@ -144138,9 +144138,28 @@ var { mulaw, alaw } = resolveAlawmulaw();
144138
144138
  * name, and WE own the TCP socket — when the broker's reader detaches we know
144139
144139
  * the viewer is gone and release the monitor at once, instead of polling the
144140
144140
  * broker for consumers or waiting on a duration cap.
144141
+ *
144142
+ * And we own the FEED: the relay dies in three ways nodedreame reports and one
144143
+ * it cannot — the socket errors (`error`; with no listener an EventEmitter
144144
+ * throws, and on 2026-09-09 a `read ETIMEDOUT` took the whole provider down),
144145
+ * the relay closes and the stream stops itself (`stopped`), or the socket goes
144146
+ * silent for minutes before the kernel gives up (nothing at all: 16 min of a
144147
+ * broker dialling a port that accepted and sent nothing). The server reports
144148
+ * every one of them ONCE through `onFeedLost`, and never its own close, so the
144149
+ * device can put the lazy sentinel back and let the next dial be a wake (D424).
144141
144150
  */
144142
144151
  var VIDEO_PT = 96;
144143
144152
  var AUDIO_PT = 97;
144153
+ /**
144154
+ * How long the feed may go without a video access unit before the relay is
144155
+ * declared dead. The broker gives a source 8 s (`RTP_ACTIVITY_TIMEOUT_MS`);
144156
+ * this sits just past it so a stall is judged by the side that can act on it.
144157
+ */
144158
+ var FEED_STALL_MS = 1e4;
144159
+ var FEED_STALL_SWEEP_MS = 1e3;
144160
+ function describeLoss(loss) {
144161
+ return loss.error === null ? loss.reason : `${loss.reason}: ${loss.error.message}`;
144162
+ }
144144
144163
  function deferred() {
144145
144164
  let resolve = () => void 0;
144146
144165
  return {
@@ -144173,11 +144192,33 @@ function consoleInto(logger) {
144173
144192
  * server calls `onConsumerDisconnect` when the last reader detaches.
144174
144193
  */
144175
144194
  async function startRfc4571Server(params) {
144176
- const { stream, logger, onConsumerDisconnect, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3 } = params;
144195
+ const { stream, logger, onConsumerDisconnect, onFeedLost, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3, stallTimeoutMs = FEED_STALL_MS } = params;
144177
144196
  const videoRef = { current: null };
144178
144197
  const audioRef = { current: null };
144179
144198
  const firstKeyframe = deferred();
144180
144199
  const firstAudio = deferred();
144200
+ const firstLoss = deferred();
144201
+ let pendingLoss = null;
144202
+ let toreDown = false;
144203
+ let serving = false;
144204
+ let lastVideoAt = 0;
144205
+ const reportLoss = (loss) => {
144206
+ if (toreDown || pendingLoss !== null) return;
144207
+ pendingLoss = loss;
144208
+ firstLoss.resolve(loss);
144209
+ if (serving) onFeedLost(loss);
144210
+ };
144211
+ stream.on("error", (err) => reportLoss({
144212
+ reason: "error",
144213
+ error: err
144214
+ }));
144215
+ stream.on("stopped", () => reportLoss({
144216
+ reason: "stopped",
144217
+ error: null
144218
+ }));
144219
+ stream.on("videoAccessUnit", () => {
144220
+ lastVideoAt = Date.now();
144221
+ });
144181
144222
  stream.on("videoAccessUnit", (e) => {
144182
144223
  if (videoRef.current !== null) return;
144183
144224
  const p = extractH264ParamSetsFromAccessUnit(e.data);
@@ -144203,8 +144244,11 @@ async function startRfc4571Server(params) {
144203
144244
  }
144204
144245
  });
144205
144246
  await stream.start();
144247
+ const lostBeforeServing = firstLoss.promise.then((loss) => {
144248
+ throw new Error(`relay lost before the first keyframe (${describeLoss(loss)})`);
144249
+ });
144206
144250
  try {
144207
- await withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe");
144251
+ await Promise.race([withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe"), lostBeforeServing]);
144208
144252
  } catch (err) {
144209
144253
  await stream.stop().catch(() => void 0);
144210
144254
  throw err;
@@ -144212,6 +144256,10 @@ async function startRfc4571Server(params) {
144212
144256
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144213
144257
  setTimeout(resolve, audioGraceMs).unref?.();
144214
144258
  })]);
144259
+ if (pendingLoss !== null) {
144260
+ await stream.stop().catch(() => void 0);
144261
+ throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
144262
+ }
144215
144263
  const paramSets = videoRef.current;
144216
144264
  if (paramSets === null) throw new Error("no parameter sets after the first keyframe");
144217
144265
  const audioFacts = audioRef.current;
@@ -144231,7 +144279,6 @@ async function startRfc4571Server(params) {
144231
144279
  if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144232
144280
  let liveClients = 0;
144233
144281
  let idleTimer = null;
144234
- let toreDown = false;
144235
144282
  const server = net.createServer((socket) => {
144236
144283
  liveClients += 1;
144237
144284
  if (idleTimer !== null) clearTimeout(idleTimer);
@@ -144258,6 +144305,15 @@ async function startRfc4571Server(params) {
144258
144305
  });
144259
144306
  const addr = server.address();
144260
144307
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144308
+ const stallSweep = setInterval(() => {
144309
+ if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144310
+ reportLoss({
144311
+ reason: "stalled",
144312
+ error: null
144313
+ });
144314
+ }, Math.max(5, Math.min(FEED_STALL_SWEEP_MS, Math.floor(stallTimeoutMs / 2))));
144315
+ stallSweep.unref?.();
144316
+ serving = true;
144261
144317
  return {
144262
144318
  host: bindHost,
144263
144319
  port: addr.port,
@@ -144269,6 +144325,7 @@ async function startRfc4571Server(params) {
144269
144325
  } : null,
144270
144326
  close: async () => {
144271
144327
  toreDown = true;
144328
+ clearInterval(stallSweep);
144272
144329
  if (idleTimer !== null) clearTimeout(idleTimer);
144273
144330
  try {
144274
144331
  muxer.close();
@@ -144548,6 +144605,9 @@ var DreameCameraDevice = class extends DreameChildDevice {
144548
144605
  keyframeTimeoutMs: WAKE_TIMEOUT_MS,
144549
144606
  onConsumerDisconnect: () => {
144550
144607
  this.closeStream("idle");
144608
+ },
144609
+ onFeedLost: (loss) => {
144610
+ this.onFeedLost(loss);
144551
144611
  }
144552
144612
  });
144553
144613
  this.live = {
@@ -144587,6 +144647,22 @@ var DreameCameraDevice = class extends DreameChildDevice {
144587
144647
  });
144588
144648
  return this.opening;
144589
144649
  }
144650
+ /**
144651
+ * The feed died under an open monitor. The session is closed — the lazy
144652
+ * sentinel goes back on the broker — so the broker's next dial asks for a
144653
+ * materialize, which is the wake. Logged: this branch drops a live stream.
144654
+ */
144655
+ onFeedLost(loss) {
144656
+ this.ctx.logger.warn("robot camera: relay feed lost — closing the monitor; the next dial re-opens it", {
144657
+ tags: { deviceId: this.id },
144658
+ meta: {
144659
+ reason: loss.reason,
144660
+ error: loss.error === null ? null : errMsg(loss.error),
144661
+ openMs: Date.now() - this.sessionOpenedAt
144662
+ }
144663
+ });
144664
+ this.closeStream("feed-lost");
144665
+ }
144590
144666
  armMaxDuration() {
144591
144667
  if (this.maxTimer !== null) clearTimeout(this.maxTimer);
144592
144668
  this.maxTimer = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.92",
3
+ "version": "0.2.93",
4
4
  "description": "Dreame robot-vacuum / lawn-mower device-provider addon for CamStack — wraps the @apocaliss92/nodedreame Dreamehome cloud client",
5
5
  "keywords": [
6
6
  "camstack",