@camstack/addon-provider-dreame 0.2.93 → 0.2.95

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.
Files changed (3) hide show
  1. package/dist/addon.js +151 -10
  2. package/dist/addon.mjs +151 -10
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -69160,6 +69160,7 @@ var NcSystemEventKindSchema = _enum([
69160
69160
  "addon-crash-loop",
69161
69161
  "addon-update-available",
69162
69162
  "server-update-available",
69163
+ "wrapper-update-available",
69163
69164
  "alarm-triggered",
69164
69165
  "alarm-armed",
69165
69166
  "alarm-disarmed",
@@ -72605,7 +72606,17 @@ var EventDensityBucketSchema = object({
72605
72606
  bucketStart: number(),
72606
72607
  motion: number().int(),
72607
72608
  object: number().int(),
72608
- audio: number().int()
72609
+ audio: number().int(),
72610
+ /**
72611
+ * Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
72612
+ * as opposed to `audio`, which is their COUNT (D431).
72613
+ *
72614
+ * ABSENT when the bucket holds no audio event. Absence means "nothing was
72615
+ * measured", which is not the same claim as silence, and a lane that cannot
72616
+ * tell them apart paints a quiet hour at full scale — which is exactly what
72617
+ * the count-on-a-level-meter did.
72618
+ */
72619
+ audioDbfs: number().optional()
72609
72620
  });
72610
72621
  /**
72611
72622
  * One camera's row in a `getEventDensityBatch` answer.
@@ -74706,6 +74717,53 @@ var ImageContractSchema = object({
74706
74717
  /** One operator-grade sentence: this node runs image X; the contract says Y. */
74707
74718
  message: string()
74708
74719
  });
74720
+ /**
74721
+ * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
74722
+ * produced it.
74723
+ *
74724
+ * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
74725
+ * update is an ANNOUNCEMENT. The notification body carries the exact command
74726
+ * for a `docker` shell and says the app restarts into the new build for
74727
+ * `electron`, and that sentence is chosen from `kind` alone — a boolean
74728
+ * nothing reads is the defect this repo keeps paying for.
74729
+ */
74730
+ var WrapperIdentitySchema = object({
74731
+ kind: _enum([
74732
+ "docker",
74733
+ "electron",
74734
+ "native",
74735
+ "contradictory",
74736
+ "unknown"
74737
+ ]),
74738
+ /** Every probe that ran, in a stable order. Never empty. */
74739
+ evidence: array(object({
74740
+ /** Which probe this row reports. */
74741
+ fact: _enum([
74742
+ "dockerenv-file",
74743
+ "proc-1-cgroup",
74744
+ "platform",
74745
+ "electron-app-version-env"
74746
+ ]),
74747
+ /**
74748
+ * `observed` — this process read it itself. `declared` — the shell told the
74749
+ * process and nothing here can check it. Only the Electron app version is
74750
+ * ever `declared`, and only because a child cannot see its parent.
74751
+ */
74752
+ mode: _enum(["observed", "declared"]),
74753
+ /** Did the probe support the fact it names? A `false` row is still evidence. */
74754
+ holds: boolean(),
74755
+ /** What was actually read, verbatim enough for an operator to argue with. */
74756
+ detail: string()
74757
+ })),
74758
+ /**
74759
+ * The version of the SHELL, when the shell has one it can state: the
74760
+ * Electron app version it declared, or the baked seed closure that
74761
+ * fingerprints a container / app bundle. `null` for `contradictory` and
74762
+ * `unknown` — which version belongs to the shell is precisely what is not
74763
+ * known there.
74764
+ */
74765
+ currentVersion: string().nullable()
74766
+ });
74709
74767
  var ServerRollbackInfoSchema = object({
74710
74768
  /** The version that failed (or was manually rolled back). */
74711
74769
  fromVersion: string(),
@@ -74747,7 +74805,13 @@ var ServerPackageStatusSchema = object({
74747
74805
  * Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
74748
74806
  * version skew: an older provider's payload simply omits it.
74749
74807
  */
74750
- imageContract: ImageContractSchema.optional()
74808
+ imageContract: ImageContractSchema.optional(),
74809
+ /**
74810
+ * What this node runs INSIDE (see {@link WrapperIdentitySchema}). Optional
74811
+ * for version skew: an older provider's payload simply omits it, and an
74812
+ * absent wrapper is unknown — never `native`.
74813
+ */
74814
+ wrapper: WrapperIdentitySchema.optional()
74751
74815
  });
74752
74816
  var ServerUpdateCheckResultSchema = object({
74753
74817
  packageName: string(),
@@ -82793,7 +82857,23 @@ var RecordingRangeSchema = object({
82793
82857
  });
82794
82858
  var RecordingAvailabilitySchema = object({
82795
82859
  deviceId: number(),
82796
- ranges: array(RecordingRangeSchema)
82860
+ ranges: array(RecordingRangeSchema),
82861
+ /**
82862
+ * Every profile this camera has footage in — not only the one `ranges`
82863
+ * describes (D433).
82864
+ *
82865
+ * `ranges` answers for ONE profile by design: the timeline is a single bar,
82866
+ * and enumerating all of them triples the directory reads for a bar that
82867
+ * would look identical. But the answer used to say nothing about that, so a
82868
+ * caller asking "what can I export?" read the single preferred profile as
82869
+ * the only one that exists — on this deployment every camera records `high`
82870
+ * AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
82871
+ * UI could not see it.
82872
+ *
82873
+ * Cheap: it is the same shallow directory read the profile CHOICE already
82874
+ * makes. Ask for a specific profile's ranges with the `profile` input.
82875
+ */
82876
+ profilesWithFootage: array(string())
82797
82877
  });
82798
82878
  var RecordingDaysSchema = object({
82799
82879
  deviceId: number(),
@@ -82819,7 +82899,11 @@ var RecordingDaysSchema = object({
82819
82899
  var RecordingAvailabilityForDeviceSchema = object({
82820
82900
  deviceId: number(),
82821
82901
  read: _enum(["read", "unreadable"]),
82822
- ranges: array(RecordingRangeSchema).readonly()
82902
+ ranges: array(RecordingRangeSchema).readonly(),
82903
+ /** Same field, same meaning, as the singular answer (D433). A row that
82904
+ * dropped it would tell a grid caller the archive holds one profile.
82905
+ * Empty on an `'unreadable'` row: nobody looked. */
82906
+ profilesWithFootage: array(string()).readonly()
82823
82907
  });
82824
82908
  /**
82825
82909
  * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
@@ -83109,7 +83193,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
83109
83193
  method(object({
83110
83194
  deviceId: number(),
83111
83195
  fromMs: number(),
83112
- toMs: number()
83196
+ toMs: number(),
83197
+ /**
83198
+ * Answer for THIS profile instead of the preferred one (D433). Absent
83199
+ * keeps the timeline's behaviour — one bar, one profile, one set of
83200
+ * reads. `profilesWithFootage` on the answer says what may be asked
83201
+ * for.
83202
+ */
83203
+ profile: string().optional()
83113
83204
  }), RecordingAvailabilitySchema, {
83114
83205
  kind: "query",
83115
83206
  auth: "protected"
@@ -98475,7 +98566,7 @@ function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_E
98475
98566
  function resolveNavigationFeatures(overrides) {
98476
98567
  return {
98477
98568
  ...DREAME_NAVIGATION_DEFAULT_FEATURES,
98478
- ...overrides?.features ?? {}
98569
+ ...overrides?.features
98479
98570
  };
98480
98571
  }
98481
98572
  /** The disabled entry ids in force for ONE device: the addon set plus its own. */
@@ -98585,7 +98676,7 @@ var DreameNetworkLinkMonitor = class {
98585
98676
  meta: {
98586
98677
  reason,
98587
98678
  rawBars: bars,
98588
- ...next ?? {}
98679
+ ...next
98589
98680
  }
98590
98681
  });
98591
98682
  this.reported = true;
@@ -144203,6 +144294,10 @@ async function startRfc4571Server(params) {
144203
144294
  const firstLoss = deferred();
144204
144295
  let pendingLoss = null;
144205
144296
  let toreDown = false;
144297
+ let audioFrames = 0;
144298
+ let videoAccessUnits = 0;
144299
+ let lastAudioAt = 0;
144300
+ let audioStallReported = false;
144206
144301
  let serving = false;
144207
144302
  let lastVideoAt = 0;
144208
144303
  const reportLoss = (loss) => {
@@ -144256,9 +144351,11 @@ async function startRfc4571Server(params) {
144256
144351
  await stream.stop().catch(() => void 0);
144257
144352
  throw err;
144258
144353
  }
144354
+ const graceStartedAt = Date.now();
144259
144355
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144260
144356
  setTimeout(resolve, audioGraceMs).unref?.();
144261
144357
  })]);
144358
+ const waitedMs = Date.now() - graceStartedAt;
144262
144359
  if (pendingLoss !== null) {
144263
144360
  await stream.stop().catch(() => void 0);
144264
144361
  throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
@@ -144278,8 +144375,38 @@ async function startRfc4571Server(params) {
144278
144375
  configHex: audioFacts.configHex
144279
144376
  } : void 0);
144280
144377
  const muxer = new Rfc4571Muxer(consoleInto(logger), VIDEO_PT, audioFacts !== null ? AUDIO_PT : void 0);
144281
- stream.on("videoAccessUnit", (e) => muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds));
144282
- if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144378
+ stream.on("videoAccessUnit", (e) => {
144379
+ videoAccessUnits += 1;
144380
+ muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds);
144381
+ });
144382
+ if (audioFacts !== null) {
144383
+ logger.info("rfc4571: serving audio", { meta: {
144384
+ codec: "aac",
144385
+ sampleRate: audioFacts.sampleRate,
144386
+ channels: audioFacts.channels,
144387
+ waitedMs
144388
+ } });
144389
+ lastAudioAt = Date.now();
144390
+ stream.on("audioFrame", (adts) => {
144391
+ audioFrames += 1;
144392
+ lastAudioAt = Date.now();
144393
+ muxer.sendAudioAdtsFrame(adts);
144394
+ });
144395
+ } else {
144396
+ logger.info("rfc4571: no audio within the grace — serving video-only", { meta: {
144397
+ waitedMs,
144398
+ audioGraceMs
144399
+ } });
144400
+ let reportedLate = false;
144401
+ stream.on("audioFrame", () => {
144402
+ if (reportedLate) return;
144403
+ reportedLate = true;
144404
+ logger.warn("rfc4571: audio arrived after the grace — this session stays video-only", { meta: {
144405
+ lateByMs: Date.now() - graceStartedAt - waitedMs,
144406
+ audioGraceMs
144407
+ } });
144408
+ });
144409
+ }
144283
144410
  let liveClients = 0;
144284
144411
  let idleTimer = null;
144285
144412
  const server = node_net.default.createServer((socket) => {
@@ -144309,7 +144436,16 @@ async function startRfc4571Server(params) {
144309
144436
  const addr = server.address();
144310
144437
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144311
144438
  const stallSweep = setInterval(() => {
144312
- if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144439
+ if (toreDown) return;
144440
+ if (audioFacts !== null && !audioStallReported && Date.now() - lastAudioAt >= stallTimeoutMs) {
144441
+ audioStallReported = true;
144442
+ logger.warn("rfc4571: audio stopped while the video kept flowing", { meta: {
144443
+ audioFrames,
144444
+ videoAccessUnits,
144445
+ silentForMs: Date.now() - lastAudioAt
144446
+ } });
144447
+ }
144448
+ if (Date.now() - lastVideoAt < stallTimeoutMs) return;
144313
144449
  reportLoss({
144314
144450
  reason: "stalled",
144315
144451
  error: null
@@ -144328,6 +144464,11 @@ async function startRfc4571Server(params) {
144328
144464
  } : null,
144329
144465
  close: async () => {
144330
144466
  toreDown = true;
144467
+ logger.info("rfc4571: session ended", { meta: {
144468
+ videoAccessUnits,
144469
+ audioFrames,
144470
+ declaredAudio: audioFacts !== null
144471
+ } });
144331
144472
  clearInterval(stallSweep);
144332
144473
  if (idleTimer !== null) clearTimeout(idleTimer);
144333
144474
  try {
package/dist/addon.mjs CHANGED
@@ -69157,6 +69157,7 @@ var NcSystemEventKindSchema = _enum([
69157
69157
  "addon-crash-loop",
69158
69158
  "addon-update-available",
69159
69159
  "server-update-available",
69160
+ "wrapper-update-available",
69160
69161
  "alarm-triggered",
69161
69162
  "alarm-armed",
69162
69163
  "alarm-disarmed",
@@ -72602,7 +72603,17 @@ var EventDensityBucketSchema = object({
72602
72603
  bucketStart: number(),
72603
72604
  motion: number().int(),
72604
72605
  object: number().int(),
72605
- audio: number().int()
72606
+ audio: number().int(),
72607
+ /**
72608
+ * Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
72609
+ * as opposed to `audio`, which is their COUNT (D431).
72610
+ *
72611
+ * ABSENT when the bucket holds no audio event. Absence means "nothing was
72612
+ * measured", which is not the same claim as silence, and a lane that cannot
72613
+ * tell them apart paints a quiet hour at full scale — which is exactly what
72614
+ * the count-on-a-level-meter did.
72615
+ */
72616
+ audioDbfs: number().optional()
72606
72617
  });
72607
72618
  /**
72608
72619
  * One camera's row in a `getEventDensityBatch` answer.
@@ -74703,6 +74714,53 @@ var ImageContractSchema = object({
74703
74714
  /** One operator-grade sentence: this node runs image X; the contract says Y. */
74704
74715
  message: string()
74705
74716
  });
74717
+ /**
74718
+ * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
74719
+ * produced it.
74720
+ *
74721
+ * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
74722
+ * update is an ANNOUNCEMENT. The notification body carries the exact command
74723
+ * for a `docker` shell and says the app restarts into the new build for
74724
+ * `electron`, and that sentence is chosen from `kind` alone — a boolean
74725
+ * nothing reads is the defect this repo keeps paying for.
74726
+ */
74727
+ var WrapperIdentitySchema = object({
74728
+ kind: _enum([
74729
+ "docker",
74730
+ "electron",
74731
+ "native",
74732
+ "contradictory",
74733
+ "unknown"
74734
+ ]),
74735
+ /** Every probe that ran, in a stable order. Never empty. */
74736
+ evidence: array(object({
74737
+ /** Which probe this row reports. */
74738
+ fact: _enum([
74739
+ "dockerenv-file",
74740
+ "proc-1-cgroup",
74741
+ "platform",
74742
+ "electron-app-version-env"
74743
+ ]),
74744
+ /**
74745
+ * `observed` — this process read it itself. `declared` — the shell told the
74746
+ * process and nothing here can check it. Only the Electron app version is
74747
+ * ever `declared`, and only because a child cannot see its parent.
74748
+ */
74749
+ mode: _enum(["observed", "declared"]),
74750
+ /** Did the probe support the fact it names? A `false` row is still evidence. */
74751
+ holds: boolean(),
74752
+ /** What was actually read, verbatim enough for an operator to argue with. */
74753
+ detail: string()
74754
+ })),
74755
+ /**
74756
+ * The version of the SHELL, when the shell has one it can state: the
74757
+ * Electron app version it declared, or the baked seed closure that
74758
+ * fingerprints a container / app bundle. `null` for `contradictory` and
74759
+ * `unknown` — which version belongs to the shell is precisely what is not
74760
+ * known there.
74761
+ */
74762
+ currentVersion: string().nullable()
74763
+ });
74706
74764
  var ServerRollbackInfoSchema = object({
74707
74765
  /** The version that failed (or was manually rolled back). */
74708
74766
  fromVersion: string(),
@@ -74744,7 +74802,13 @@ var ServerPackageStatusSchema = object({
74744
74802
  * Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
74745
74803
  * version skew: an older provider's payload simply omits it.
74746
74804
  */
74747
- imageContract: ImageContractSchema.optional()
74805
+ imageContract: ImageContractSchema.optional(),
74806
+ /**
74807
+ * What this node runs INSIDE (see {@link WrapperIdentitySchema}). Optional
74808
+ * for version skew: an older provider's payload simply omits it, and an
74809
+ * absent wrapper is unknown — never `native`.
74810
+ */
74811
+ wrapper: WrapperIdentitySchema.optional()
74748
74812
  });
74749
74813
  var ServerUpdateCheckResultSchema = object({
74750
74814
  packageName: string(),
@@ -82790,7 +82854,23 @@ var RecordingRangeSchema = object({
82790
82854
  });
82791
82855
  var RecordingAvailabilitySchema = object({
82792
82856
  deviceId: number(),
82793
- ranges: array(RecordingRangeSchema)
82857
+ ranges: array(RecordingRangeSchema),
82858
+ /**
82859
+ * Every profile this camera has footage in — not only the one `ranges`
82860
+ * describes (D433).
82861
+ *
82862
+ * `ranges` answers for ONE profile by design: the timeline is a single bar,
82863
+ * and enumerating all of them triples the directory reads for a bar that
82864
+ * would look identical. But the answer used to say nothing about that, so a
82865
+ * caller asking "what can I export?" read the single preferred profile as
82866
+ * the only one that exists — on this deployment every camera records `high`
82867
+ * AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
82868
+ * UI could not see it.
82869
+ *
82870
+ * Cheap: it is the same shallow directory read the profile CHOICE already
82871
+ * makes. Ask for a specific profile's ranges with the `profile` input.
82872
+ */
82873
+ profilesWithFootage: array(string())
82794
82874
  });
82795
82875
  var RecordingDaysSchema = object({
82796
82876
  deviceId: number(),
@@ -82816,7 +82896,11 @@ var RecordingDaysSchema = object({
82816
82896
  var RecordingAvailabilityForDeviceSchema = object({
82817
82897
  deviceId: number(),
82818
82898
  read: _enum(["read", "unreadable"]),
82819
- ranges: array(RecordingRangeSchema).readonly()
82899
+ ranges: array(RecordingRangeSchema).readonly(),
82900
+ /** Same field, same meaning, as the singular answer (D433). A row that
82901
+ * dropped it would tell a grid caller the archive holds one profile.
82902
+ * Empty on an `'unreadable'` row: nobody looked. */
82903
+ profilesWithFootage: array(string()).readonly()
82820
82904
  });
82821
82905
  /**
82822
82906
  * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
@@ -83106,7 +83190,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
83106
83190
  method(object({
83107
83191
  deviceId: number(),
83108
83192
  fromMs: number(),
83109
- toMs: number()
83193
+ toMs: number(),
83194
+ /**
83195
+ * Answer for THIS profile instead of the preferred one (D433). Absent
83196
+ * keeps the timeline's behaviour — one bar, one profile, one set of
83197
+ * reads. `profilesWithFootage` on the answer says what may be asked
83198
+ * for.
83199
+ */
83200
+ profile: string().optional()
83110
83201
  }), RecordingAvailabilitySchema, {
83111
83202
  kind: "query",
83112
83203
  auth: "protected"
@@ -98472,7 +98563,7 @@ function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_E
98472
98563
  function resolveNavigationFeatures(overrides) {
98473
98564
  return {
98474
98565
  ...DREAME_NAVIGATION_DEFAULT_FEATURES,
98475
- ...overrides?.features ?? {}
98566
+ ...overrides?.features
98476
98567
  };
98477
98568
  }
98478
98569
  /** The disabled entry ids in force for ONE device: the addon set plus its own. */
@@ -98582,7 +98673,7 @@ var DreameNetworkLinkMonitor = class {
98582
98673
  meta: {
98583
98674
  reason,
98584
98675
  rawBars: bars,
98585
- ...next ?? {}
98676
+ ...next
98586
98677
  }
98587
98678
  });
98588
98679
  this.reported = true;
@@ -144200,6 +144291,10 @@ async function startRfc4571Server(params) {
144200
144291
  const firstLoss = deferred();
144201
144292
  let pendingLoss = null;
144202
144293
  let toreDown = false;
144294
+ let audioFrames = 0;
144295
+ let videoAccessUnits = 0;
144296
+ let lastAudioAt = 0;
144297
+ let audioStallReported = false;
144203
144298
  let serving = false;
144204
144299
  let lastVideoAt = 0;
144205
144300
  const reportLoss = (loss) => {
@@ -144253,9 +144348,11 @@ async function startRfc4571Server(params) {
144253
144348
  await stream.stop().catch(() => void 0);
144254
144349
  throw err;
144255
144350
  }
144351
+ const graceStartedAt = Date.now();
144256
144352
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144257
144353
  setTimeout(resolve, audioGraceMs).unref?.();
144258
144354
  })]);
144355
+ const waitedMs = Date.now() - graceStartedAt;
144259
144356
  if (pendingLoss !== null) {
144260
144357
  await stream.stop().catch(() => void 0);
144261
144358
  throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
@@ -144275,8 +144372,38 @@ async function startRfc4571Server(params) {
144275
144372
  configHex: audioFacts.configHex
144276
144373
  } : void 0);
144277
144374
  const muxer = new Rfc4571Muxer(consoleInto(logger), VIDEO_PT, audioFacts !== null ? AUDIO_PT : void 0);
144278
- stream.on("videoAccessUnit", (e) => muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds));
144279
- if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144375
+ stream.on("videoAccessUnit", (e) => {
144376
+ videoAccessUnits += 1;
144377
+ muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds);
144378
+ });
144379
+ if (audioFacts !== null) {
144380
+ logger.info("rfc4571: serving audio", { meta: {
144381
+ codec: "aac",
144382
+ sampleRate: audioFacts.sampleRate,
144383
+ channels: audioFacts.channels,
144384
+ waitedMs
144385
+ } });
144386
+ lastAudioAt = Date.now();
144387
+ stream.on("audioFrame", (adts) => {
144388
+ audioFrames += 1;
144389
+ lastAudioAt = Date.now();
144390
+ muxer.sendAudioAdtsFrame(adts);
144391
+ });
144392
+ } else {
144393
+ logger.info("rfc4571: no audio within the grace — serving video-only", { meta: {
144394
+ waitedMs,
144395
+ audioGraceMs
144396
+ } });
144397
+ let reportedLate = false;
144398
+ stream.on("audioFrame", () => {
144399
+ if (reportedLate) return;
144400
+ reportedLate = true;
144401
+ logger.warn("rfc4571: audio arrived after the grace — this session stays video-only", { meta: {
144402
+ lateByMs: Date.now() - graceStartedAt - waitedMs,
144403
+ audioGraceMs
144404
+ } });
144405
+ });
144406
+ }
144280
144407
  let liveClients = 0;
144281
144408
  let idleTimer = null;
144282
144409
  const server = net.createServer((socket) => {
@@ -144306,7 +144433,16 @@ async function startRfc4571Server(params) {
144306
144433
  const addr = server.address();
144307
144434
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144308
144435
  const stallSweep = setInterval(() => {
144309
- if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144436
+ if (toreDown) return;
144437
+ if (audioFacts !== null && !audioStallReported && Date.now() - lastAudioAt >= stallTimeoutMs) {
144438
+ audioStallReported = true;
144439
+ logger.warn("rfc4571: audio stopped while the video kept flowing", { meta: {
144440
+ audioFrames,
144441
+ videoAccessUnits,
144442
+ silentForMs: Date.now() - lastAudioAt
144443
+ } });
144444
+ }
144445
+ if (Date.now() - lastVideoAt < stallTimeoutMs) return;
144310
144446
  reportLoss({
144311
144447
  reason: "stalled",
144312
144448
  error: null
@@ -144325,6 +144461,11 @@ async function startRfc4571Server(params) {
144325
144461
  } : null,
144326
144462
  close: async () => {
144327
144463
  toreDown = true;
144464
+ logger.info("rfc4571: session ended", { meta: {
144465
+ videoAccessUnits,
144466
+ audioFrames,
144467
+ declaredAudio: audioFacts !== null
144468
+ } });
144328
144469
  clearInterval(stallSweep);
144329
144470
  if (idleTimer !== null) clearTimeout(idleTimer);
144330
144471
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.93",
3
+ "version": "0.2.95",
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",