@camstack/addon-provider-dreame 0.2.93 → 0.2.94
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 +96 -9
- package/dist/addon.mjs +96 -9
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -72605,7 +72605,17 @@ var EventDensityBucketSchema = object({
|
|
|
72605
72605
|
bucketStart: number(),
|
|
72606
72606
|
motion: number().int(),
|
|
72607
72607
|
object: number().int(),
|
|
72608
|
-
audio: number().int()
|
|
72608
|
+
audio: number().int(),
|
|
72609
|
+
/**
|
|
72610
|
+
* Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
|
|
72611
|
+
* as opposed to `audio`, which is their COUNT (D431).
|
|
72612
|
+
*
|
|
72613
|
+
* ABSENT when the bucket holds no audio event. Absence means "nothing was
|
|
72614
|
+
* measured", which is not the same claim as silence, and a lane that cannot
|
|
72615
|
+
* tell them apart paints a quiet hour at full scale — which is exactly what
|
|
72616
|
+
* the count-on-a-level-meter did.
|
|
72617
|
+
*/
|
|
72618
|
+
audioDbfs: number().optional()
|
|
72609
72619
|
});
|
|
72610
72620
|
/**
|
|
72611
72621
|
* One camera's row in a `getEventDensityBatch` answer.
|
|
@@ -82793,7 +82803,23 @@ var RecordingRangeSchema = object({
|
|
|
82793
82803
|
});
|
|
82794
82804
|
var RecordingAvailabilitySchema = object({
|
|
82795
82805
|
deviceId: number(),
|
|
82796
|
-
ranges: array(RecordingRangeSchema)
|
|
82806
|
+
ranges: array(RecordingRangeSchema),
|
|
82807
|
+
/**
|
|
82808
|
+
* Every profile this camera has footage in — not only the one `ranges`
|
|
82809
|
+
* describes (D433).
|
|
82810
|
+
*
|
|
82811
|
+
* `ranges` answers for ONE profile by design: the timeline is a single bar,
|
|
82812
|
+
* and enumerating all of them triples the directory reads for a bar that
|
|
82813
|
+
* would look identical. But the answer used to say nothing about that, so a
|
|
82814
|
+
* caller asking "what can I export?" read the single preferred profile as
|
|
82815
|
+
* the only one that exists — on this deployment every camera records `high`
|
|
82816
|
+
* AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
|
|
82817
|
+
* UI could not see it.
|
|
82818
|
+
*
|
|
82819
|
+
* Cheap: it is the same shallow directory read the profile CHOICE already
|
|
82820
|
+
* makes. Ask for a specific profile's ranges with the `profile` input.
|
|
82821
|
+
*/
|
|
82822
|
+
profilesWithFootage: array(string())
|
|
82797
82823
|
});
|
|
82798
82824
|
var RecordingDaysSchema = object({
|
|
82799
82825
|
deviceId: number(),
|
|
@@ -82819,7 +82845,11 @@ var RecordingDaysSchema = object({
|
|
|
82819
82845
|
var RecordingAvailabilityForDeviceSchema = object({
|
|
82820
82846
|
deviceId: number(),
|
|
82821
82847
|
read: _enum(["read", "unreadable"]),
|
|
82822
|
-
ranges: array(RecordingRangeSchema).readonly()
|
|
82848
|
+
ranges: array(RecordingRangeSchema).readonly(),
|
|
82849
|
+
/** Same field, same meaning, as the singular answer (D433). A row that
|
|
82850
|
+
* dropped it would tell a grid caller the archive holds one profile.
|
|
82851
|
+
* Empty on an `'unreadable'` row: nobody looked. */
|
|
82852
|
+
profilesWithFootage: array(string()).readonly()
|
|
82823
82853
|
});
|
|
82824
82854
|
/**
|
|
82825
82855
|
* One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
|
|
@@ -83109,7 +83139,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
|
|
|
83109
83139
|
method(object({
|
|
83110
83140
|
deviceId: number(),
|
|
83111
83141
|
fromMs: number(),
|
|
83112
|
-
toMs: number()
|
|
83142
|
+
toMs: number(),
|
|
83143
|
+
/**
|
|
83144
|
+
* Answer for THIS profile instead of the preferred one (D433). Absent
|
|
83145
|
+
* keeps the timeline's behaviour — one bar, one profile, one set of
|
|
83146
|
+
* reads. `profilesWithFootage` on the answer says what may be asked
|
|
83147
|
+
* for.
|
|
83148
|
+
*/
|
|
83149
|
+
profile: string().optional()
|
|
83113
83150
|
}), RecordingAvailabilitySchema, {
|
|
83114
83151
|
kind: "query",
|
|
83115
83152
|
auth: "protected"
|
|
@@ -98475,7 +98512,7 @@ function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_E
|
|
|
98475
98512
|
function resolveNavigationFeatures(overrides) {
|
|
98476
98513
|
return {
|
|
98477
98514
|
...DREAME_NAVIGATION_DEFAULT_FEATURES,
|
|
98478
|
-
...overrides?.features
|
|
98515
|
+
...overrides?.features
|
|
98479
98516
|
};
|
|
98480
98517
|
}
|
|
98481
98518
|
/** The disabled entry ids in force for ONE device: the addon set plus its own. */
|
|
@@ -98585,7 +98622,7 @@ var DreameNetworkLinkMonitor = class {
|
|
|
98585
98622
|
meta: {
|
|
98586
98623
|
reason,
|
|
98587
98624
|
rawBars: bars,
|
|
98588
|
-
...next
|
|
98625
|
+
...next
|
|
98589
98626
|
}
|
|
98590
98627
|
});
|
|
98591
98628
|
this.reported = true;
|
|
@@ -144203,6 +144240,10 @@ async function startRfc4571Server(params) {
|
|
|
144203
144240
|
const firstLoss = deferred();
|
|
144204
144241
|
let pendingLoss = null;
|
|
144205
144242
|
let toreDown = false;
|
|
144243
|
+
let audioFrames = 0;
|
|
144244
|
+
let videoAccessUnits = 0;
|
|
144245
|
+
let lastAudioAt = 0;
|
|
144246
|
+
let audioStallReported = false;
|
|
144206
144247
|
let serving = false;
|
|
144207
144248
|
let lastVideoAt = 0;
|
|
144208
144249
|
const reportLoss = (loss) => {
|
|
@@ -144256,9 +144297,11 @@ async function startRfc4571Server(params) {
|
|
|
144256
144297
|
await stream.stop().catch(() => void 0);
|
|
144257
144298
|
throw err;
|
|
144258
144299
|
}
|
|
144300
|
+
const graceStartedAt = Date.now();
|
|
144259
144301
|
await Promise.race([firstAudio.promise, new Promise((resolve) => {
|
|
144260
144302
|
setTimeout(resolve, audioGraceMs).unref?.();
|
|
144261
144303
|
})]);
|
|
144304
|
+
const waitedMs = Date.now() - graceStartedAt;
|
|
144262
144305
|
if (pendingLoss !== null) {
|
|
144263
144306
|
await stream.stop().catch(() => void 0);
|
|
144264
144307
|
throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
|
|
@@ -144278,8 +144321,38 @@ async function startRfc4571Server(params) {
|
|
|
144278
144321
|
configHex: audioFacts.configHex
|
|
144279
144322
|
} : void 0);
|
|
144280
144323
|
const muxer = new Rfc4571Muxer(consoleInto(logger), VIDEO_PT, audioFacts !== null ? AUDIO_PT : void 0);
|
|
144281
|
-
stream.on("videoAccessUnit", (e) =>
|
|
144282
|
-
|
|
144324
|
+
stream.on("videoAccessUnit", (e) => {
|
|
144325
|
+
videoAccessUnits += 1;
|
|
144326
|
+
muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds);
|
|
144327
|
+
});
|
|
144328
|
+
if (audioFacts !== null) {
|
|
144329
|
+
logger.info("rfc4571: serving audio", { meta: {
|
|
144330
|
+
codec: "aac",
|
|
144331
|
+
sampleRate: audioFacts.sampleRate,
|
|
144332
|
+
channels: audioFacts.channels,
|
|
144333
|
+
waitedMs
|
|
144334
|
+
} });
|
|
144335
|
+
lastAudioAt = Date.now();
|
|
144336
|
+
stream.on("audioFrame", (adts) => {
|
|
144337
|
+
audioFrames += 1;
|
|
144338
|
+
lastAudioAt = Date.now();
|
|
144339
|
+
muxer.sendAudioAdtsFrame(adts);
|
|
144340
|
+
});
|
|
144341
|
+
} else {
|
|
144342
|
+
logger.info("rfc4571: no audio within the grace — serving video-only", { meta: {
|
|
144343
|
+
waitedMs,
|
|
144344
|
+
audioGraceMs
|
|
144345
|
+
} });
|
|
144346
|
+
let reportedLate = false;
|
|
144347
|
+
stream.on("audioFrame", () => {
|
|
144348
|
+
if (reportedLate) return;
|
|
144349
|
+
reportedLate = true;
|
|
144350
|
+
logger.warn("rfc4571: audio arrived after the grace — this session stays video-only", { meta: {
|
|
144351
|
+
lateByMs: Date.now() - graceStartedAt - waitedMs,
|
|
144352
|
+
audioGraceMs
|
|
144353
|
+
} });
|
|
144354
|
+
});
|
|
144355
|
+
}
|
|
144283
144356
|
let liveClients = 0;
|
|
144284
144357
|
let idleTimer = null;
|
|
144285
144358
|
const server = node_net.default.createServer((socket) => {
|
|
@@ -144309,7 +144382,16 @@ async function startRfc4571Server(params) {
|
|
|
144309
144382
|
const addr = server.address();
|
|
144310
144383
|
if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
|
|
144311
144384
|
const stallSweep = setInterval(() => {
|
|
144312
|
-
if (toreDown
|
|
144385
|
+
if (toreDown) return;
|
|
144386
|
+
if (audioFacts !== null && !audioStallReported && Date.now() - lastAudioAt >= stallTimeoutMs) {
|
|
144387
|
+
audioStallReported = true;
|
|
144388
|
+
logger.warn("rfc4571: audio stopped while the video kept flowing", { meta: {
|
|
144389
|
+
audioFrames,
|
|
144390
|
+
videoAccessUnits,
|
|
144391
|
+
silentForMs: Date.now() - lastAudioAt
|
|
144392
|
+
} });
|
|
144393
|
+
}
|
|
144394
|
+
if (Date.now() - lastVideoAt < stallTimeoutMs) return;
|
|
144313
144395
|
reportLoss({
|
|
144314
144396
|
reason: "stalled",
|
|
144315
144397
|
error: null
|
|
@@ -144328,6 +144410,11 @@ async function startRfc4571Server(params) {
|
|
|
144328
144410
|
} : null,
|
|
144329
144411
|
close: async () => {
|
|
144330
144412
|
toreDown = true;
|
|
144413
|
+
logger.info("rfc4571: session ended", { meta: {
|
|
144414
|
+
videoAccessUnits,
|
|
144415
|
+
audioFrames,
|
|
144416
|
+
declaredAudio: audioFacts !== null
|
|
144417
|
+
} });
|
|
144331
144418
|
clearInterval(stallSweep);
|
|
144332
144419
|
if (idleTimer !== null) clearTimeout(idleTimer);
|
|
144333
144420
|
try {
|
package/dist/addon.mjs
CHANGED
|
@@ -72602,7 +72602,17 @@ var EventDensityBucketSchema = object({
|
|
|
72602
72602
|
bucketStart: number(),
|
|
72603
72603
|
motion: number().int(),
|
|
72604
72604
|
object: number().int(),
|
|
72605
|
-
audio: number().int()
|
|
72605
|
+
audio: number().int(),
|
|
72606
|
+
/**
|
|
72607
|
+
* Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
|
|
72608
|
+
* as opposed to `audio`, which is their COUNT (D431).
|
|
72609
|
+
*
|
|
72610
|
+
* ABSENT when the bucket holds no audio event. Absence means "nothing was
|
|
72611
|
+
* measured", which is not the same claim as silence, and a lane that cannot
|
|
72612
|
+
* tell them apart paints a quiet hour at full scale — which is exactly what
|
|
72613
|
+
* the count-on-a-level-meter did.
|
|
72614
|
+
*/
|
|
72615
|
+
audioDbfs: number().optional()
|
|
72606
72616
|
});
|
|
72607
72617
|
/**
|
|
72608
72618
|
* One camera's row in a `getEventDensityBatch` answer.
|
|
@@ -82790,7 +82800,23 @@ var RecordingRangeSchema = object({
|
|
|
82790
82800
|
});
|
|
82791
82801
|
var RecordingAvailabilitySchema = object({
|
|
82792
82802
|
deviceId: number(),
|
|
82793
|
-
ranges: array(RecordingRangeSchema)
|
|
82803
|
+
ranges: array(RecordingRangeSchema),
|
|
82804
|
+
/**
|
|
82805
|
+
* Every profile this camera has footage in — not only the one `ranges`
|
|
82806
|
+
* describes (D433).
|
|
82807
|
+
*
|
|
82808
|
+
* `ranges` answers for ONE profile by design: the timeline is a single bar,
|
|
82809
|
+
* and enumerating all of them triples the directory reads for a bar that
|
|
82810
|
+
* would look identical. But the answer used to say nothing about that, so a
|
|
82811
|
+
* caller asking "what can I export?" read the single preferred profile as
|
|
82812
|
+
* the only one that exists — on this deployment every camera records `high`
|
|
82813
|
+
* AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
|
|
82814
|
+
* UI could not see it.
|
|
82815
|
+
*
|
|
82816
|
+
* Cheap: it is the same shallow directory read the profile CHOICE already
|
|
82817
|
+
* makes. Ask for a specific profile's ranges with the `profile` input.
|
|
82818
|
+
*/
|
|
82819
|
+
profilesWithFootage: array(string())
|
|
82794
82820
|
});
|
|
82795
82821
|
var RecordingDaysSchema = object({
|
|
82796
82822
|
deviceId: number(),
|
|
@@ -82816,7 +82842,11 @@ var RecordingDaysSchema = object({
|
|
|
82816
82842
|
var RecordingAvailabilityForDeviceSchema = object({
|
|
82817
82843
|
deviceId: number(),
|
|
82818
82844
|
read: _enum(["read", "unreadable"]),
|
|
82819
|
-
ranges: array(RecordingRangeSchema).readonly()
|
|
82845
|
+
ranges: array(RecordingRangeSchema).readonly(),
|
|
82846
|
+
/** Same field, same meaning, as the singular answer (D433). A row that
|
|
82847
|
+
* dropped it would tell a grid caller the archive holds one profile.
|
|
82848
|
+
* Empty on an `'unreadable'` row: nobody looked. */
|
|
82849
|
+
profilesWithFootage: array(string()).readonly()
|
|
82820
82850
|
});
|
|
82821
82851
|
/**
|
|
82822
82852
|
* One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
|
|
@@ -83106,7 +83136,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
|
|
|
83106
83136
|
method(object({
|
|
83107
83137
|
deviceId: number(),
|
|
83108
83138
|
fromMs: number(),
|
|
83109
|
-
toMs: number()
|
|
83139
|
+
toMs: number(),
|
|
83140
|
+
/**
|
|
83141
|
+
* Answer for THIS profile instead of the preferred one (D433). Absent
|
|
83142
|
+
* keeps the timeline's behaviour — one bar, one profile, one set of
|
|
83143
|
+
* reads. `profilesWithFootage` on the answer says what may be asked
|
|
83144
|
+
* for.
|
|
83145
|
+
*/
|
|
83146
|
+
profile: string().optional()
|
|
83110
83147
|
}), RecordingAvailabilitySchema, {
|
|
83111
83148
|
kind: "query",
|
|
83112
83149
|
auth: "protected"
|
|
@@ -98472,7 +98509,7 @@ function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_E
|
|
|
98472
98509
|
function resolveNavigationFeatures(overrides) {
|
|
98473
98510
|
return {
|
|
98474
98511
|
...DREAME_NAVIGATION_DEFAULT_FEATURES,
|
|
98475
|
-
...overrides?.features
|
|
98512
|
+
...overrides?.features
|
|
98476
98513
|
};
|
|
98477
98514
|
}
|
|
98478
98515
|
/** The disabled entry ids in force for ONE device: the addon set plus its own. */
|
|
@@ -98582,7 +98619,7 @@ var DreameNetworkLinkMonitor = class {
|
|
|
98582
98619
|
meta: {
|
|
98583
98620
|
reason,
|
|
98584
98621
|
rawBars: bars,
|
|
98585
|
-
...next
|
|
98622
|
+
...next
|
|
98586
98623
|
}
|
|
98587
98624
|
});
|
|
98588
98625
|
this.reported = true;
|
|
@@ -144200,6 +144237,10 @@ async function startRfc4571Server(params) {
|
|
|
144200
144237
|
const firstLoss = deferred();
|
|
144201
144238
|
let pendingLoss = null;
|
|
144202
144239
|
let toreDown = false;
|
|
144240
|
+
let audioFrames = 0;
|
|
144241
|
+
let videoAccessUnits = 0;
|
|
144242
|
+
let lastAudioAt = 0;
|
|
144243
|
+
let audioStallReported = false;
|
|
144203
144244
|
let serving = false;
|
|
144204
144245
|
let lastVideoAt = 0;
|
|
144205
144246
|
const reportLoss = (loss) => {
|
|
@@ -144253,9 +144294,11 @@ async function startRfc4571Server(params) {
|
|
|
144253
144294
|
await stream.stop().catch(() => void 0);
|
|
144254
144295
|
throw err;
|
|
144255
144296
|
}
|
|
144297
|
+
const graceStartedAt = Date.now();
|
|
144256
144298
|
await Promise.race([firstAudio.promise, new Promise((resolve) => {
|
|
144257
144299
|
setTimeout(resolve, audioGraceMs).unref?.();
|
|
144258
144300
|
})]);
|
|
144301
|
+
const waitedMs = Date.now() - graceStartedAt;
|
|
144259
144302
|
if (pendingLoss !== null) {
|
|
144260
144303
|
await stream.stop().catch(() => void 0);
|
|
144261
144304
|
throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
|
|
@@ -144275,8 +144318,38 @@ async function startRfc4571Server(params) {
|
|
|
144275
144318
|
configHex: audioFacts.configHex
|
|
144276
144319
|
} : void 0);
|
|
144277
144320
|
const muxer = new Rfc4571Muxer(consoleInto(logger), VIDEO_PT, audioFacts !== null ? AUDIO_PT : void 0);
|
|
144278
|
-
stream.on("videoAccessUnit", (e) =>
|
|
144279
|
-
|
|
144321
|
+
stream.on("videoAccessUnit", (e) => {
|
|
144322
|
+
videoAccessUnits += 1;
|
|
144323
|
+
muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds);
|
|
144324
|
+
});
|
|
144325
|
+
if (audioFacts !== null) {
|
|
144326
|
+
logger.info("rfc4571: serving audio", { meta: {
|
|
144327
|
+
codec: "aac",
|
|
144328
|
+
sampleRate: audioFacts.sampleRate,
|
|
144329
|
+
channels: audioFacts.channels,
|
|
144330
|
+
waitedMs
|
|
144331
|
+
} });
|
|
144332
|
+
lastAudioAt = Date.now();
|
|
144333
|
+
stream.on("audioFrame", (adts) => {
|
|
144334
|
+
audioFrames += 1;
|
|
144335
|
+
lastAudioAt = Date.now();
|
|
144336
|
+
muxer.sendAudioAdtsFrame(adts);
|
|
144337
|
+
});
|
|
144338
|
+
} else {
|
|
144339
|
+
logger.info("rfc4571: no audio within the grace — serving video-only", { meta: {
|
|
144340
|
+
waitedMs,
|
|
144341
|
+
audioGraceMs
|
|
144342
|
+
} });
|
|
144343
|
+
let reportedLate = false;
|
|
144344
|
+
stream.on("audioFrame", () => {
|
|
144345
|
+
if (reportedLate) return;
|
|
144346
|
+
reportedLate = true;
|
|
144347
|
+
logger.warn("rfc4571: audio arrived after the grace — this session stays video-only", { meta: {
|
|
144348
|
+
lateByMs: Date.now() - graceStartedAt - waitedMs,
|
|
144349
|
+
audioGraceMs
|
|
144350
|
+
} });
|
|
144351
|
+
});
|
|
144352
|
+
}
|
|
144280
144353
|
let liveClients = 0;
|
|
144281
144354
|
let idleTimer = null;
|
|
144282
144355
|
const server = net.createServer((socket) => {
|
|
@@ -144306,7 +144379,16 @@ async function startRfc4571Server(params) {
|
|
|
144306
144379
|
const addr = server.address();
|
|
144307
144380
|
if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
|
|
144308
144381
|
const stallSweep = setInterval(() => {
|
|
144309
|
-
if (toreDown
|
|
144382
|
+
if (toreDown) return;
|
|
144383
|
+
if (audioFacts !== null && !audioStallReported && Date.now() - lastAudioAt >= stallTimeoutMs) {
|
|
144384
|
+
audioStallReported = true;
|
|
144385
|
+
logger.warn("rfc4571: audio stopped while the video kept flowing", { meta: {
|
|
144386
|
+
audioFrames,
|
|
144387
|
+
videoAccessUnits,
|
|
144388
|
+
silentForMs: Date.now() - lastAudioAt
|
|
144389
|
+
} });
|
|
144390
|
+
}
|
|
144391
|
+
if (Date.now() - lastVideoAt < stallTimeoutMs) return;
|
|
144310
144392
|
reportLoss({
|
|
144311
144393
|
reason: "stalled",
|
|
144312
144394
|
error: null
|
|
@@ -144325,6 +144407,11 @@ async function startRfc4571Server(params) {
|
|
|
144325
144407
|
} : null,
|
|
144326
144408
|
close: async () => {
|
|
144327
144409
|
toreDown = true;
|
|
144410
|
+
logger.info("rfc4571: session ended", { meta: {
|
|
144411
|
+
videoAccessUnits,
|
|
144412
|
+
audioFrames,
|
|
144413
|
+
declaredAudio: audioFacts !== null
|
|
144414
|
+
} });
|
|
144328
144415
|
clearInterval(stallSweep);
|
|
144329
144416
|
if (idleTimer !== null) clearTimeout(idleTimer);
|
|
144330
144417
|
try {
|
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.94",
|
|
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",
|