@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 +79 -3
- package/dist/addon.mjs +79 -3
- package/package.json +1 -1
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.
|
|
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",
|