@camstack/addon-pipeline 1.2.89 → 1.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.
Files changed (30) hide show
  1. package/dist/{addon-utils-yc-chXuC.js → addon-utils-CTRpEiSd.js} +1 -1
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +1 -1
  4. package/dist/detection-pipeline/index.js +4 -4
  5. package/dist/detection-pipeline/index.mjs +2 -2
  6. package/dist/{dist-BJYwWEZ9.mjs → dist-6POVIYmC.mjs} +91 -4
  7. package/dist/{dist-n4Nbnxx5.js → dist-D9To_r_d.js} +96 -3
  8. package/dist/{event-loop-stall-monitor-BiPffk0D.mjs → event-loop-stall-monitor-C-VvWOxS.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-DG7QY00k.js → event-loop-stall-monitor-CGacbzXq.js} +1 -1
  10. package/dist/{lazy-sharp-EoSOHgoj.js → lazy-sharp-C3zOnmiU.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/recorder/index.js +680 -37
  16. package/dist/recorder/index.mjs +679 -36
  17. package/dist/session-decode/decode-worker-child.js +2 -2
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CsrMZYLB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2woB6bY.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dut56EeQ.mjs +26 -0
  22. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DgivrPsH.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DsIi4G5Q.mjs} +1 -1
  23. package/dist/stream-broker/{hostInit-DoFovQmc.mjs → hostInit-VCURcZLt.mjs} +2 -2
  24. package/dist/stream-broker/index.js +194 -30
  25. package/dist/stream-broker/index.mjs +194 -30
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/dist/{worker-protocol-DjuxhGo3.js → worker-protocol-Bq28qobd.js} +1 -1
  28. package/dist/{worker-protocol-XZ1e36rM.mjs → worker-protocol-PzAKg8mC.mjs} +1 -1
  29. package/package.json +1 -1
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DNSELPGi.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-n4Nbnxx5.js");
5
+ const require_dist = require("../dist-D9To_r_d.js");
6
6
  const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_dist.__toESM(node_crypto, 1);
@@ -1298,6 +1298,36 @@ function buildDerivedTranscodeArgs(input) {
1298
1298
  audio
1299
1299
  };
1300
1300
  }
1301
+ //#endregion
1302
+ //#region src/stream-broker/fill-empty-profile-slots.ts
1303
+ /**
1304
+ * When `assignProfile` locks `auto: false` (putting a derived on `low`), a
1305
+ * later catalog blip can empty the other slots. Auto-rank must not overwrite
1306
+ * the operator's slot, but EMPTY slots should still take unused eligible
1307
+ * natives — otherwise the camera stays dark until someone clicks the
1308
+ * dropdowns again (618 / 1438).
1309
+ */
1310
+ function fillEmptyProfileSlots(current, autoRanked) {
1311
+ const used = /* @__PURE__ */ new Set();
1312
+ for (const profile of require_dist.CAM_PROFILE_ORDER) {
1313
+ const id = current[profile];
1314
+ if (id) used.add(id);
1315
+ }
1316
+ const ranked = [];
1317
+ for (const profile of require_dist.CAM_PROFILE_ORDER) {
1318
+ const id = autoRanked[profile];
1319
+ if (id && !ranked.includes(id)) ranked.push(id);
1320
+ }
1321
+ const next = { ...current };
1322
+ for (const profile of require_dist.CAM_PROFILE_ORDER) {
1323
+ if (next[profile]) continue;
1324
+ const candidate = ranked.find((id) => !used.has(id));
1325
+ if (!candidate) continue;
1326
+ next[profile] = candidate;
1327
+ used.add(candidate);
1328
+ }
1329
+ return next;
1330
+ }
1301
1331
  /**
1302
1332
  * Ceiling for a configured pre-roll.
1303
1333
  *
@@ -13715,8 +13745,7 @@ var TranscodePipelineManager = class {
13715
13745
  const outputArgs = input.outputArgs ?? [];
13716
13746
  const audioEgress = input.audioEgress === true;
13717
13747
  const sourceStream = input.profile !== void 0 ? this.resolveTargetedStream(input.deviceId, input.profile) : input.targetResolution !== void 0 ? this.pickClosestSource(input.deviceId, input.targetResolution) ?? this.firstPublished(input.deviceId) : this.firstPublished(input.deviceId);
13718
- if (input.profile === void 0 && !sourceStream) throw new Error(`getStreamWithCodec: no published stream for device ${input.deviceId}`);
13719
- if (!sourceStream || !sourceStream.url) throw new Error(`getStreamWithCodec: no assigned source for device ${input.deviceId}`);
13748
+ if (!sourceStream) throw new Error(input.profile === void 0 ? `getStreamWithCodec: no published stream for device ${input.deviceId}` : `getStreamWithCodec: no assigned source for device ${input.deviceId}`);
13720
13749
  const sourceCodec = normaliseCodec(sourceStream.codec);
13721
13750
  const outResolution = input.targetResolution ?? sourceStream.resolution ?? {
13722
13751
  width: 1280,
@@ -13901,11 +13930,17 @@ var TranscodePipelineManager = class {
13901
13930
  * Throws when the profile has no assignment or the assigned source is not
13902
13931
  * currently published — a targeted request must not silently fall back to
13903
13932
  * a different source.
13933
+ *
13934
+ * Publication is "in the catalog", NOT `!!stream.url`. A `derived:*` stream
13935
+ * is broker-owned metadata with no camera pull URL; consumers ride the
13936
+ * broker restream (`resolveSourceUrl`), and the broker starts the transcode
13937
+ * feeder on demand. Requiring a catalog `url` made recording/WebRTC throw
13938
+ * `not published` for a live derived assignment (618, 2026-08-17).
13904
13939
  */
13905
13940
  resolveTargetedStream(deviceId, profile) {
13906
13941
  const camStreamId = this.resolveProfileSource?.(deviceId, profile) ?? null;
13907
13942
  if (!camStreamId) throw new Error(`getStreamWithCodec: profile "${profile}" has no assigned source for device ${deviceId}`);
13908
- const stream = this.cameraStreamLookup(deviceId).find((s) => s.camStreamId === camStreamId && !!s.url);
13943
+ const stream = this.cameraStreamLookup(deviceId).find((s) => s.camStreamId === camStreamId);
13909
13944
  if (!stream) throw new Error(`getStreamWithCodec: profile "${profile}" source "${camStreamId}" not published for device ${deviceId}`);
13910
13945
  return stream;
13911
13946
  }
@@ -14571,6 +14606,10 @@ var StreamBrokerManager = class StreamBrokerManager {
14571
14606
  return this.transcodeManager;
14572
14607
  }
14573
14608
  async getStreamWithCodec(input) {
14609
+ if (input.profile !== void 0) {
14610
+ const camStreamId = this.assignments.get(input.deviceId)?.map[input.profile];
14611
+ if (camStreamId) await this.ensureDerivedReady(input.deviceId, camStreamId);
14612
+ }
14574
14613
  const source = await this.getTranscodeManager().acquire(input);
14575
14614
  if (input.hostname !== void 0 && input.hostname.length > 0) return {
14576
14615
  ...source,
@@ -15237,8 +15276,14 @@ var StreamBrokerManager = class StreamBrokerManager {
15237
15276
  */
15238
15277
  async ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId, tier) {
15239
15278
  if (this.disabledDevices.has(deviceId)) return;
15240
- if (this.brokers.has(brokerId)) return;
15241
15279
  if (this.remoteBrokerAdapters.has(brokerId)) return;
15280
+ if (camStreamId !== void 0) {
15281
+ if ((this.cameraStreams.get(deviceId)?.get(camStreamId))?.kind === "derived") {
15282
+ await this.ensureDerivedReady(deviceId, camStreamId);
15283
+ if (this.brokers.has(brokerId)) return;
15284
+ }
15285
+ }
15286
+ if (this.brokers.has(brokerId)) return;
15242
15287
  if (camStreamId === void 0) return;
15243
15288
  const published = this.cameraStreams.get(deviceId)?.get(camStreamId);
15244
15289
  if (published !== void 0 && published.kind !== "derived") {
@@ -15593,7 +15638,16 @@ var StreamBrokerManager = class StreamBrokerManager {
15593
15638
  camStreamId: input.sourceCamStreamId
15594
15639
  } : void 0);
15595
15640
  if (!derivedSource || !encodeProfile) throw new Error("kind: 'derived' requires a source (profile|camStream) + encodeProfile");
15596
- if (this.cameraStreams.get(deviceId)?.get(camStreamId)) return { success: true };
15641
+ const existingDerived = this.cameraStreams.get(deviceId)?.get(camStreamId);
15642
+ if (existingDerived) {
15643
+ this.cameraStreams.get(deviceId)?.set(camStreamId, {
15644
+ ...existingDerived,
15645
+ derivedSource,
15646
+ encodeProfile
15647
+ });
15648
+ await this.ensureDerivedReady(deviceId, camStreamId);
15649
+ return { success: true };
15650
+ }
15597
15651
  if (derivedSource.kind === "camStream") {
15598
15652
  const sourceBrokerId = brokerIdFor(deviceId, derivedSource.camStreamId);
15599
15653
  if (!this.brokers.has(sourceBrokerId)) throw new Error(`SOURCE_NOT_FOUND: ${sourceBrokerId}`);
@@ -15619,11 +15673,14 @@ var StreamBrokerManager = class StreamBrokerManager {
15619
15673
  order,
15620
15674
  deviceFeatures: deviceFeaturesArr,
15621
15675
  autoEligible: input.autoEligible ?? false,
15622
- brokerOwned: true
15676
+ brokerOwned: true,
15677
+ derivedSource,
15678
+ encodeProfile
15623
15679
  };
15624
15680
  derivedStreamMap.set(camStreamId, derivedCam);
15625
15681
  await this.ensureBroker(deviceId, camStreamId);
15626
15682
  this.attachDerivedFeeder(deviceId, camStreamId, derivedSource, encodeProfile);
15683
+ await this.reconcileAutoAssignment(deviceId, "publish", true);
15627
15684
  this.emitCamStreamsChanged(deviceId);
15628
15685
  return { success: true };
15629
15686
  }
@@ -17784,14 +17841,15 @@ var StreamBrokerManager = class StreamBrokerManager {
17784
17841
  *
17785
17842
  * Streams marked `autoEligible: false` are skipped — they remain
17786
17843
  * published (visible + manually assignable via `assignProfile`) but
17787
- * never become the default. If every stream is non-eligible the
17788
- * algorithm degrades gracefully and considers them all (so a device
17789
- * with only ineligible streams still gets some default assignment).
17844
+ * never become the default. If every stream is non-eligible the map
17845
+ * stays empty: ranking ineligible streams (a derived that beat the
17846
+ * catalog to boot) parked a transcode on `high` and left the natives
17847
+ * around it (618, 2026-08-17).
17790
17848
  */
17791
17849
  computeInitialAssignment(streams) {
17792
- if (streams.length === 0) return {};
17793
17850
  const eligible = streams.filter((s) => s.autoEligible);
17794
- const ranked = [...eligible.length > 0 ? eligible : streams].toSorted((a, b) => {
17851
+ if (eligible.length === 0) return {};
17852
+ const ranked = [...eligible].toSorted((a, b) => {
17795
17853
  const aPx = a.resolution ? a.resolution.width * a.resolution.height : -1;
17796
17854
  const bPx = b.resolution ? b.resolution.width * b.resolution.height : -1;
17797
17855
  if (aPx !== bPx) return bPx - aPx;
@@ -17814,13 +17872,19 @@ var StreamBrokerManager = class StreamBrokerManager {
17814
17872
  * `publishCameraStream` so a multi-stream catalog publish can upsert
17815
17873
  * every stream first and rank ONCE (no transient intermediate
17816
17874
  * assignment). When the device's assignment is operator-locked
17817
- * (`auto === false`) the existing map is re-applied unchanged.
17875
+ * (`auto === false`) occupied slots stay put (a derived on `low` must
17876
+ * not be kicked off). Empty slots refill from unused natives when the
17877
+ * device has a derived stream (618 / 1438) or the whole map is empty —
17878
+ * a camera without deriveds still honours an operator unassign.
17818
17879
  */
17819
17880
  async reconcileAutoAssignment(deviceId, reason, streamAdded = false) {
17820
17881
  const current = this.assignments.get(deviceId);
17821
17882
  const streams = [...this.cameraStreams.get(deviceId)?.values() ?? []].toSorted((a, b) => a.order - b.order);
17822
17883
  const isAuto = current?.auto ?? true;
17823
- const newMap = !current || isAuto && this.wouldChangeAutoMap(current.map, streams) ? this.computeInitialAssignment(streams) : current.map;
17884
+ const ranked = this.computeInitialAssignment(streams);
17885
+ const hasDerived = streams.some((s) => s.kind === "derived");
17886
+ const lockedEmpty = current !== void 0 && !isAuto && require_dist.CAM_PROFILE_ORDER.every((profile) => current.map[profile] === void 0);
17887
+ const newMap = !current ? ranked : isAuto ? this.wouldChangeAutoMap(current.map, streams) ? ranked : current.map : hasDerived || lockedEmpty ? fillEmptyProfileSlots(current.map, ranked) : current.map;
17824
17888
  await this.applyAssignmentUpdate(deviceId, newMap, {
17825
17889
  auto: isAuto,
17826
17890
  reason,
@@ -18219,6 +18283,10 @@ var StreamBrokerManager = class StreamBrokerManager {
18219
18283
  if (!existingToken) this.rtspProvider.persistTokens();
18220
18284
  await this.fanOutRegistration(deviceId, brokerId, cam, broker);
18221
18285
  this.syncProfileRestreamers(deviceId);
18286
+ if (cam.kind === "derived") {
18287
+ const def = this.resolveDerivedDef(deviceId, camStreamId);
18288
+ if (def) this.attachDerivedFeeder(deviceId, camStreamId, def.source, def.encodeProfile);
18289
+ }
18222
18290
  }
18223
18291
  /**
18224
18292
  * Tear down the broker for a `(deviceId, camStreamId)` pair. No-op
@@ -18250,6 +18318,40 @@ var StreamBrokerManager = class StreamBrokerManager {
18250
18318
  this.syncProfileRestreamers(deviceId);
18251
18319
  }
18252
18320
  /**
18321
+ * Ensure a derived stream's push broker exists AND its transcode feeder is
18322
+ * attached. Consumers (recording `getStreamWithCodec`, WebRTC session setup,
18323
+ * profile restream DESCRIBE) address the PROFILE; the broker is responsible
18324
+ * for starting the derived. No-op for non-derived camStreams.
18325
+ */
18326
+ async ensureDerivedReady(deviceId, camStreamId) {
18327
+ if ((this.cameraStreams.get(deviceId)?.get(camStreamId))?.kind !== "derived") return;
18328
+ const def = this.resolveDerivedDef(deviceId, camStreamId);
18329
+ if (!def) return;
18330
+ await this.ensureBroker(deviceId, camStreamId);
18331
+ this.attachDerivedFeeder(deviceId, camStreamId, def.source, def.encodeProfile);
18332
+ }
18333
+ /**
18334
+ * Source + encode profile for a derived camStream. Prefers the fields stored
18335
+ * on the catalog entry at publish; falls back to the persisted device
18336
+ * override (`derivedStreams[<name>]`) so a consumer can re-attach after a
18337
+ * broker release even if the in-memory record predates those fields.
18338
+ */
18339
+ resolveDerivedDef(deviceId, camStreamId) {
18340
+ const published = this.cameraStreams.get(deviceId)?.get(camStreamId);
18341
+ if (published?.derivedSource && published.encodeProfile) return {
18342
+ source: published.derivedSource,
18343
+ encodeProfile: published.encodeProfile
18344
+ };
18345
+ if (!camStreamId.startsWith("derived:")) return null;
18346
+ const name = camStreamId.slice(8);
18347
+ const def = this.deviceOverrides.get(deviceId)?.derivedStreams?.[name];
18348
+ if (!def?.source || !def.encodeProfile) return null;
18349
+ return {
18350
+ source: def.source,
18351
+ encodeProfile: def.encodeProfile
18352
+ };
18353
+ }
18354
+ /**
18253
18355
  * Attach the on-demand transcode-feeder lifecycle to a freshly-minted derived
18254
18356
  * push broker. The feeder runs ONLY while the derived broker is consumed — it
18255
18357
  * is spawned on the broker's demand edge (`onDemandChange(true)`) and torn
@@ -25006,6 +25108,22 @@ var RecordedFeeder = class {
25006
25108
  }
25007
25109
  };
25008
25110
  //#endregion
25111
+ //#region src/stream-broker/recorded/playback-profile-fallback.ts
25112
+ /**
25113
+ * Recorded playback is pinned to one profile (no adaptive ladder). When the
25114
+ * viewer asks for `mid` (auto/adaptive default) but the camera only kept
25115
+ * `high` — typical of events-mode — locate must try the other tiers before
25116
+ * reporting a gap.
25117
+ */
25118
+ var PLAYBACK_PROFILE_FALLBACK_ORDER = [
25119
+ "high",
25120
+ "mid",
25121
+ "low"
25122
+ ];
25123
+ function profileSearchOrder(requested) {
25124
+ return [requested, ...PLAYBACK_PROFILE_FALLBACK_ORDER.filter((p) => p !== requested)];
25125
+ }
25126
+ //#endregion
25009
25127
  //#region src/stream-broker/recorded/timeline-session.ts
25010
25128
  /**
25011
25129
  * Per-viewer coordinator over one broker WebRTC session. Owns the playback mode
@@ -25052,17 +25170,14 @@ var TimelineSession = class {
25052
25170
  async handleControl(msg) {
25053
25171
  this.emitLiveProfile();
25054
25172
  switch (msg.t) {
25055
- case "playRecorded": {
25056
- const profileChanged = msg.profile !== this.profile;
25057
- this.profile = msg.profile;
25058
- await (this.mode === "recorded" && this.feeder && !profileChanged ? this.feeder : this.enterRecorded()).seek(msg.epoch, { travelFromMs: this.lastPositionMs });
25173
+ case "playRecorded":
25174
+ await this.playAt(msg.epoch, msg.profile, { travelFromMs: this.lastPositionMs });
25059
25175
  break;
25060
- }
25061
25176
  case "seek":
25062
- await (this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).seek(msg.epoch, { travelFromMs: this.lastPositionMs });
25177
+ await this.playAt(msg.epoch, this.profile, { travelFromMs: this.lastPositionMs });
25063
25178
  break;
25064
25179
  case "scrubCommit":
25065
- await (this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).seek(msg.epoch);
25180
+ await this.playAt(msg.epoch, this.profile);
25066
25181
  break;
25067
25182
  case "scrubStart":
25068
25183
  (this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).beginScrub();
@@ -25108,6 +25223,29 @@ var TimelineSession = class {
25108
25223
  this.feeder = feeder;
25109
25224
  return feeder;
25110
25225
  }
25226
+ /**
25227
+ * Play recorded footage at `epochMs`. If `locate` is wired and the requested
25228
+ * profile has no segment, try high → mid → low so events-mode cameras that
25229
+ * only kept one tier still play.
25230
+ */
25231
+ async playAt(epochMs, requestedProfile, opts) {
25232
+ const profile = await this.resolveProfile(requestedProfile, epochMs);
25233
+ const profileChanged = profile !== this.profile;
25234
+ this.profile = profile;
25235
+ const feeder = this.mode === "recorded" && this.feeder && !profileChanged ? this.feeder : this.enterRecorded();
25236
+ if (opts) await feeder.seek(epochMs, opts);
25237
+ else await feeder.seek(epochMs);
25238
+ }
25239
+ async resolveProfile(requested, epochMs) {
25240
+ const locate = this.deps.locate;
25241
+ if (!locate) return requested;
25242
+ for (const profile of profileSearchOrder(requested)) if ((await locate({
25243
+ deviceId: this.deps.deviceId,
25244
+ profile,
25245
+ epochMs
25246
+ })).kind === "segment") return profile;
25247
+ return requested;
25248
+ }
25111
25249
  goLive() {
25112
25250
  this.feeder?.dispose();
25113
25251
  this.feeder = null;
@@ -32786,6 +32924,30 @@ async function demuxSegmentAudioOpusPreferJs(bytes, opts = {}) {
32786
32924
  }
32787
32925
  }
32788
32926
  //#endregion
32927
+ //#region src/stream-broker/webrtc/prefer-live-adaptive-candidates.ts
32928
+ function isDerivedBrokerId(brokerId) {
32929
+ const slash = brokerId.indexOf("/");
32930
+ if (slash < 0) return false;
32931
+ return brokerId.slice(slash + 1).startsWith("derived:");
32932
+ }
32933
+ /**
32934
+ * Adaptive live walks every registered broker for a device. When no profile
32935
+ * slot is bound, Map insertion order wins — and a derived materialized at
32936
+ * boot is often first, so the picture attaches to a transcode whose source
32937
+ * profile is empty (618, 2026-08-17).
32938
+ *
32939
+ * Prefer brokers that currently own a profile tier. If none do, drop
32940
+ * unassigned deriveds so a native pull can win. If the derived is the only
32941
+ * broker, keep it (better a stream than a throw).
32942
+ */
32943
+ function preferLiveAdaptiveCandidates(candidates) {
32944
+ if (candidates.length <= 1) return [...candidates];
32945
+ const assigned = candidates.filter((c) => c.tier !== null);
32946
+ if (assigned.length > 0) return assigned;
32947
+ const natives = candidates.filter((c) => !isDerivedBrokerId(c.brokerId));
32948
+ return natives.length > 0 ? natives : [...candidates];
32949
+ }
32950
+ //#endregion
32789
32951
  //#region src/stream-broker/webrtc/broker-webrtc-server.ts
32790
32952
  /**
32791
32953
  * Broker-integrated WebRTC server.
@@ -33741,6 +33903,7 @@ var BrokerWebrtcServer = class {
33741
33903
  timeline = new TimelineSession({
33742
33904
  session,
33743
33905
  deviceId,
33906
+ locate: locateRecorded,
33744
33907
  ...initialLiveProfile !== null ? { initialLiveProfile } : {},
33745
33908
  warm: readGop ? (profile, epochMs) => {
33746
33909
  warmFragment({
@@ -34110,12 +34273,13 @@ var BrokerWebrtcServer = class {
34110
34273
  broker
34111
34274
  });
34112
34275
  }
34113
- if (candidates.length === 0) return `${deviceKey}/high`;
34114
- if (candidates.length === 1) return candidates[0].brokerId;
34276
+ const pool = preferLiveAdaptiveCandidates(candidates);
34277
+ if (pool.length === 0) return `${deviceKey}/high`;
34278
+ if (pool.length === 1) return pool[0].brokerId;
34115
34279
  const adaptiveDeviceIdTag = Number(deviceKey.split("/")[0]);
34116
34280
  const deviceIdTag = Number.isFinite(adaptiveDeviceIdTag) ? { tags: { deviceId: adaptiveDeviceIdTag } } : {};
34117
34281
  if (hints.prefersTier === "high" || hints.prefersTier === "mid" || hints.prefersTier === "low") {
34118
- const explicit = candidates.find((c) => c.tier === hints.prefersTier);
34282
+ const explicit = pool.find((c) => c.tier === hints.prefersTier);
34119
34283
  if (explicit) {
34120
34284
  this.logger.info("Adaptive source selected (prefersTier exact)", {
34121
34285
  ...deviceIdTag,
@@ -34129,7 +34293,7 @@ var BrokerWebrtcServer = class {
34129
34293
  return explicit.brokerId;
34130
34294
  }
34131
34295
  for (const tier of SMALLEST_FIRST_TIER_PREFERENCE) {
34132
- const match = candidates.find((c) => c.tier === tier);
34296
+ const match = pool.find((c) => c.tier === tier);
34133
34297
  if (match) {
34134
34298
  this.logger.info("Adaptive source selected (prefersTier fallback → smallest available)", {
34135
34299
  ...deviceIdTag,
@@ -34146,7 +34310,7 @@ var BrokerWebrtcServer = class {
34146
34310
  }
34147
34311
  if (!(hints.viewportWidth !== void 0 || hints.viewportHeight !== void 0 || hints.downlinkMbps !== void 0)) {
34148
34312
  for (const tier of TIER_PREFERENCE) {
34149
- const match = candidates.find((c) => c.tier === tier);
34313
+ const match = pool.find((c) => c.tier === tier);
34150
34314
  if (match) {
34151
34315
  this.logger.info("Adaptive source selected (no client hints — tier preference)", {
34152
34316
  ...deviceIdTag,
@@ -34159,12 +34323,12 @@ var BrokerWebrtcServer = class {
34159
34323
  return match.brokerId;
34160
34324
  }
34161
34325
  }
34162
- return candidates[0].brokerId;
34326
+ return pool[0].brokerId;
34163
34327
  }
34164
- let bestId = candidates[0].brokerId;
34165
- let bestTier = candidates[0].tier;
34328
+ let bestId = pool[0].brokerId;
34329
+ let bestTier = pool[0].tier;
34166
34330
  let bestScore = Infinity;
34167
- for (const c of candidates) {
34331
+ for (const c of pool) {
34168
34332
  const score = scoreBroker(c.tier, c.broker, hints);
34169
34333
  const cRank = c.tier ? TIER_PREFERENCE.indexOf(c.tier) : TIER_PREFERENCE.length;
34170
34334
  const bestRank = bestTier ? TIER_PREFERENCE.indexOf(bestTier) : TIER_PREFERENCE.length;