@camstack/addon-pipeline 1.2.107 → 1.2.115

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 (35) hide show
  1. package/dist/{addon-utils-Vv2dCqA2.js → addon-utils-C0ht2KPO.js} +51 -15
  2. package/dist/{addon-utils-A2S9D7pu.mjs → addon-utils-CgBCF-mE.mjs} +50 -14
  3. package/dist/audio-analyzer/index.js +3 -3
  4. package/dist/audio-analyzer/index.mjs +3 -3
  5. package/dist/detection-pipeline/index.js +170 -31
  6. package/dist/detection-pipeline/index.mjs +169 -30
  7. package/dist/{dist-L-1LMGQs.js → dist-BqIbYW1A.js} +130 -17
  8. package/dist/{dist-CkXAzTZ2.mjs → dist-DGQkkWc6.mjs} +130 -17
  9. package/dist/{event-loop-stall-monitor-fHkfgmlw.mjs → event-loop-stall-monitor-D5jK5v4Z.mjs} +223 -2
  10. package/dist/{event-loop-stall-monitor-DwAho7HD.js → event-loop-stall-monitor-ylMGpz48.js} +234 -1
  11. package/dist/{lazy-sharp-DfbyQxET.js → lazy-sharp-cOsy7l_P.js} +1 -1
  12. package/dist/motion-wasm/index.js +2 -2
  13. package/dist/motion-wasm/index.mjs +1 -1
  14. package/dist/pipeline-runner/index.js +915 -107
  15. package/dist/pipeline-runner/index.mjs +914 -106
  16. package/dist/{process-memory-CgrbLo3l.js → process-memory-45juuHpN.js} +1 -1
  17. package/dist/{process-memory-DlSOzuHP.mjs → process-memory-BtT7WfbJ.mjs} +1 -1
  18. package/dist/recorder/index.js +187 -47
  19. package/dist/recorder/index.mjs +187 -47
  20. package/dist/session-decode/decode-worker-child.js +30 -10
  21. package/dist/session-decode/decode-worker-child.mjs +29 -9
  22. package/dist/stream-broker/_stub.js +2 -2
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BeL4_jgH.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_SFFNGT.mjs} +3 -3
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B1LDFHY9.mjs +26 -0
  25. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CmHtm5CX.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BAMyzCWw.mjs} +1 -1
  26. package/dist/stream-broker/{hostInit-BK9nzKAG.mjs → hostInit-CG28NvlV.mjs} +3 -3
  27. package/dist/stream-broker/index.js +279 -198
  28. package/dist/stream-broker/index.mjs +279 -198
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/dist/{worker-protocol-GGnzfWu3.js → worker-protocol-CIrqGbpG.js} +6 -3
  31. package/dist/{worker-protocol-bEipQT9l.mjs → worker-protocol-DyocTIx_.mjs} +6 -3
  32. package/package.json +1 -1
  33. package/python/inference_pool.py +4 -1
  34. package/python/test_inference_pool_preprocess.py +87 -16
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DjndFc5V.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-L-1LMGQs.js");
5
+ const require_dist = require("../dist-BqIbYW1A.js");
6
6
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
8
  const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
@@ -23631,7 +23631,7 @@ var MonotonicClock = class {
23631
23631
  this.rate = rate;
23632
23632
  }
23633
23633
  };
23634
- var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 24 * 1024 * 1024;
23634
+ var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 48 * 1024 * 1024;
23635
23635
  var SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES = 2 * 1024 * 1024;
23636
23636
  /** Demuxed bytes an entry holds — what the byte bound actually counts. */
23637
23637
  function fragmentBytes(frag) {
@@ -24053,14 +24053,22 @@ var SCRUB_WHOLE_SEGMENT_MAX_BYTES = 15e5;
24053
24053
  * drag starts on.
24054
24054
  *
24055
24055
  * Asymmetric because the gesture is: a drag that begins at the live edge — or
24056
- * at a notification's moment — is looking for what ALREADY happened. 90 s back
24057
- * against 30 s forward is 12 segments ≈ 2.8 MB at `low`, read sequentially in
24058
- * the background while the operator is still deciding where to drag.
24056
+ * at a notification's moment — is looking for what ALREADY happened. 15 min
24057
+ * back against 5 min forward fills the 48 MB window where the finger will
24058
+ * actually move, read sequentially in the background while the first ticks
24059
+ * still pay their own demand-read.
24059
24060
  */
24060
- var SCRUB_WARM_BACK_MS = 9e4;
24061
- var SCRUB_WARM_FWD_MS = 3e4;
24062
- /** Hard bound on warm reads, whatever the spans above resolve to. */
24063
- var SCRUB_WARM_MAX_SEGMENTS = 14;
24061
+ var SCRUB_WARM_BACK_MS = 15 * 6e4;
24062
+ var SCRUB_WARM_FWD_MS = 5 * 6e4;
24063
+ /** Symmetric span used after a jump: scrub around the landing, both ways. */
24064
+ var SCRUB_WARM_AROUND_MS = 15 * 6e4;
24065
+ /**
24066
+ * Re-centre the fill when a tick lands this far from the window we are filling.
24067
+ * A slow drag stays inside this; a jump to 8am does not.
24068
+ */
24069
+ var SCRUB_RECENTER_MS = 12e4;
24070
+ /** Hard bound on warm reads, derived from the session RAM budget. */
24071
+ var SCRUB_WARM_MAX_SEGMENTS = Math.max(1, Math.floor(SCRUB_FRAGMENT_CACHE_MAX_BYTES / 2e5));
24064
24072
  /**
24065
24073
  * Median recent read time at or above which the feeder STOPS speculating.
24066
24074
  *
@@ -24314,6 +24322,16 @@ var RecordedFeeder = class {
24314
24322
  scrubReadMsWindow = [];
24315
24323
  /** Warm-up runs once per drag, from the playhead the finger went down on. */
24316
24324
  scrubWarmStarted = false;
24325
+ /** Centre of the fill currently in flight (or last completed). */
24326
+ scrubWarmCentreMs = null;
24327
+ /** Bumped to abort a fill without aborting the drag tick (`scrubToken`). */
24328
+ scrubWarmGen = 0;
24329
+ /** Tick demand-read in flight: the fill waits so the finger wins the spindle. */
24330
+ scrubTickReadInFlight = false;
24331
+ /** One fill walker per drag — a fling retargets it, it does not spawn another. */
24332
+ scrubFillLoopActive = false;
24333
+ scrubWarmBackMs = SCRUB_WARM_BACK_MS;
24334
+ scrubWarmFwdMs = SCRUB_WARM_FWD_MS;
24317
24335
  constructor(deps) {
24318
24336
  this.deps = deps;
24319
24337
  this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3);
@@ -24435,6 +24453,7 @@ var RecordedFeeder = class {
24435
24453
  let prefixFed = false;
24436
24454
  let gopFirst = false;
24437
24455
  let stateAnnounced = false;
24456
+ let reusedWhole = null;
24438
24457
  if (this.deps.readGop) {
24439
24458
  const cached = this.scrubCache.covering(this.deps.profile, landMs);
24440
24459
  let fragment;
@@ -24444,6 +24463,10 @@ var RecordedFeeder = class {
24444
24463
  if (fragmentLoad === null) return;
24445
24464
  fragment = fragmentLoad.seg;
24446
24465
  this.scrubCache.put(this.deps.profile, fragment);
24466
+ if (fragmentLoad.wholeSegmentRead && fragmentLoad.rawBytes) reusedWhole = {
24467
+ video: fragment.aus,
24468
+ bytes: fragmentLoad.rawBytes
24469
+ };
24447
24470
  }
24448
24471
  if (this.disposed || token !== this.seekToken) return;
24449
24472
  const fragmentLoc = {
@@ -24461,14 +24484,23 @@ var RecordedFeeder = class {
24461
24484
  this.startPositionReports();
24462
24485
  stateAnnounced = true;
24463
24486
  }
24464
- const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
24465
- if (this.disposed || token !== this.seekToken) return;
24466
- this.feedPrefix(loc, video, landMs);
24467
- prefixFed = true;
24468
- firstFrameMs = performance.now() - seekStartedAt;
24469
- });
24470
- fullLoad.catch(() => {});
24471
- const loaded = await fullLoad;
24487
+ let loaded;
24488
+ if (reusedWhole) {
24489
+ const audio = this.rate !== 0 && this.deps.demuxAudio && this.resolveAudioPush() !== void 0 ? await this.withTimeout(this.deps.demuxAudio(reusedWhole.bytes)).catch(() => []) : [];
24490
+ loaded = {
24491
+ video: reusedWhole.video,
24492
+ audio
24493
+ };
24494
+ } else {
24495
+ const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
24496
+ if (this.disposed || token !== this.seekToken) return;
24497
+ this.feedPrefix(loc, video, landMs);
24498
+ prefixFed = true;
24499
+ firstFrameMs = performance.now() - seekStartedAt;
24500
+ });
24501
+ fullLoad.catch(() => {});
24502
+ loaded = await fullLoad;
24503
+ }
24472
24504
  loadMs = performance.now() - loadStartedAt;
24473
24505
  if (this.disposed || token !== this.seekToken || loaded === null) return;
24474
24506
  if (!prefixFed) this.feedPrefix(loc, loaded.video, landMs);
@@ -24630,38 +24662,92 @@ var RecordedFeeder = class {
24630
24662
  * back and {@link SCRUB_WARM_FWD_MS} forward from the entry playhead, one
24631
24663
  * segment at a time, backward first.
24632
24664
  *
24633
- * Once per drag, always in the background, and never on the tick's path — a
24634
- * tick that arrives against a cold cache pays its own read exactly as before.
24665
+ * Fire-and-forget, never on the tick's path — a tick that arrives against a
24666
+ * cold cache pays its own read exactly as before. A later jump more than
24667
+ * {@link SCRUB_RECENTER_MS} from this centre aborts the fill (via
24668
+ * {@link scrubWarmGen}, not the drag token) and starts a new window.
24669
+ *
24635
24670
  * It walks segment to segment through `locate` rather than assuming a segment
24636
24671
  * length, so a recorder configured with a different `segmentSeconds` warms the
24637
24672
  * same SPAN rather than the same count.
24638
24673
  */
24639
- startScrubWarm(centreMs, gen) {
24640
- if (this.scrubWarmStarted) return;
24674
+ startScrubWarm(centreMs, dragGen, opts) {
24675
+ if (this.scrubWarmStarted && !opts?.force) return;
24641
24676
  this.scrubWarmStarted = true;
24642
- this.warmScrubWindow(centreMs, gen);
24677
+ this.scrubWarmGen += 1;
24678
+ this.scrubWarmCentreMs = centreMs;
24679
+ this.scrubWarmBackMs = opts?.backMs ?? SCRUB_WARM_BACK_MS;
24680
+ this.scrubWarmFwdMs = opts?.fwdMs ?? SCRUB_WARM_FWD_MS;
24681
+ this.deps.onLog?.("scrub-feeder:warm-start", {
24682
+ centreMs,
24683
+ fillGen: this.scrubWarmGen
24684
+ });
24685
+ if (this.scrubFillLoopActive) return;
24686
+ this.scrubFillLoopActive = true;
24687
+ this.runScrubFillLoop(dragGen);
24688
+ }
24689
+ /**
24690
+ * A single sequential walk. Recentre bumps {@link scrubWarmGen} and this loop
24691
+ * restarts around the new cursor — it must never `void` a second walker, or a
24692
+ * coarse fling stacks one USB walk per tick and play/scrub starve (1.2.113:
24693
+ * 16 walkers, seek `loadMs=16652`, drags with `ticks=0`).
24694
+ */
24695
+ async runScrubFillLoop(dragGen) {
24696
+ let lastWorked = -1;
24697
+ try {
24698
+ while (!this.disposed && this.scrubbing && dragGen === this.scrubToken) {
24699
+ const gen = this.scrubWarmGen;
24700
+ const centre = this.scrubWarmCentreMs;
24701
+ if (centre === null) return;
24702
+ await this.warmScrubWindow(centre, dragGen, gen, this.scrubWarmBackMs, this.scrubWarmFwdMs);
24703
+ lastWorked = gen;
24704
+ if (gen === this.scrubWarmGen) return;
24705
+ }
24706
+ } finally {
24707
+ this.scrubFillLoopActive = false;
24708
+ if (!this.disposed && this.scrubbing && dragGen === this.scrubToken && this.scrubWarmCentreMs !== null && this.scrubWarmGen !== lastWorked) {
24709
+ this.scrubFillLoopActive = true;
24710
+ this.runScrubFillLoop(dragGen);
24711
+ }
24712
+ }
24713
+ }
24714
+ /**
24715
+ * Abort the current fill and start a new ±15 min window around `centreMs`.
24716
+ * Does not bump `scrubToken` — the in-flight tick must still land.
24717
+ */
24718
+ recenterScrubWarm(centreMs) {
24719
+ if (this.deps.profile !== SCRUB_WHOLE_SEGMENT_PROFILE) return;
24720
+ this.startScrubWarm(centreMs, this.scrubToken, {
24721
+ force: true,
24722
+ backMs: SCRUB_WARM_AROUND_MS,
24723
+ fwdMs: SCRUB_WARM_AROUND_MS
24724
+ });
24725
+ }
24726
+ maybeRecenterScrubWarm(targetMs) {
24727
+ if (this.deps.profile !== SCRUB_WHOLE_SEGMENT_PROFILE) return;
24728
+ const centre = this.scrubWarmCentreMs;
24729
+ if (centre === null) return;
24730
+ if (Math.abs(targetMs - centre) <= SCRUB_RECENTER_MS) return;
24731
+ this.recenterScrubWarm(targetMs);
24643
24732
  }
24644
- async warmScrubWindow(centreMs, gen) {
24645
- const spent = await this.warmScrubDirection(centreMs, -1, SCRUB_WARM_BACK_MS, 0, gen);
24646
- await this.warmScrubDirection(centreMs, 1, SCRUB_WARM_FWD_MS, spent, gen);
24733
+ async warmScrubWindow(centreMs, dragGen, fillGen, backMs, fwdMs) {
24734
+ const spent = await this.warmScrubDirection(centreMs, -1, backMs, 0, dragGen, fillGen);
24735
+ await this.warmScrubDirection(centreMs, 1, fwdMs, spent, dragGen, fillGen);
24647
24736
  }
24648
24737
  /**
24649
24738
  * Walk `dir` from `fromMs` until `spanMs` of timeline is warm (or the shared
24650
24739
  * bound is hit), reading each segment that is not already cached. Returns the
24651
24740
  * running total of segments read, so the two directions share one budget.
24652
24741
  */
24653
- async warmScrubDirection(fromMs, dir, spanMs, spent, gen) {
24742
+ async warmScrubDirection(fromMs, dir, spanMs, spent, dragGen, fillGen) {
24654
24743
  let read = spent;
24655
24744
  let probeMs = fromMs;
24656
24745
  for (let i = 0; i < SCRUB_WARM_MAX_SEGMENTS; i++) {
24657
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return read;
24746
+ if (this.disposed || !this.scrubbing || dragGen !== this.scrubToken) return read;
24747
+ if (fillGen !== this.scrubWarmGen) return read;
24658
24748
  if (read >= SCRUB_WARM_MAX_SEGMENTS) return read;
24659
24749
  if (Math.abs(probeMs - fromMs) > spanMs) return read;
24660
- if (this.scrubReadsAreSlow()) {
24661
- this.scrubStats?.recordPrefetchSuppressed();
24662
- return read;
24663
- }
24664
- const seg = await this.warmOneScrubSegment(probeMs, gen);
24750
+ const seg = await this.warmOneScrubSegment(probeMs, dragGen, fillGen);
24665
24751
  if (seg === null) return read;
24666
24752
  if (seg.fetched) read += 1;
24667
24753
  probeMs = dir === -1 ? seg.startMs - 1 : seg.startMs + seg.durMs + 1;
@@ -24675,7 +24761,7 @@ var RecordedFeeder = class {
24675
24761
  * so the walk can step to its neighbour, or null when the walk must stop (a
24676
24762
  * gap, a failure, or teardown).
24677
24763
  */
24678
- async warmOneScrubSegment(probeMs, gen) {
24764
+ async warmOneScrubSegment(probeMs, dragGen, fillGen) {
24679
24765
  try {
24680
24766
  const warm = this.scrubCache.covering(this.deps.profile, probeMs);
24681
24767
  if (warm) return {
@@ -24683,16 +24769,20 @@ var RecordedFeeder = class {
24683
24769
  durMs: warm.durMs,
24684
24770
  fetched: false
24685
24771
  };
24772
+ if (!await this.waitForTickReadIdle(fillGen)) return null;
24686
24773
  const loc = await this.withTimeout(this.deps.locate({
24687
24774
  deviceId: this.deps.deviceId,
24688
24775
  profile: this.deps.profile,
24689
24776
  epochMs: probeMs
24690
24777
  }));
24691
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return null;
24778
+ if (this.disposed || !this.scrubbing || dragGen !== this.scrubToken) return null;
24779
+ if (fillGen !== this.scrubWarmGen) return null;
24692
24780
  if (loc.kind !== "segment") return null;
24781
+ if (!await this.waitForTickReadIdle(fillGen)) return null;
24693
24782
  const load = await this.loadScrubFragment(loc, probeMs);
24694
24783
  if (load !== null) this.scrubStats?.recordPrefetchRead(load.readBytes);
24695
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken || load === null) return null;
24784
+ if (this.disposed || !this.scrubbing || dragGen !== this.scrubToken || load === null) return null;
24785
+ if (fillGen !== this.scrubWarmGen) return null;
24696
24786
  this.scrubCache.put(this.deps.profile, load.seg);
24697
24787
  this.scrubPrefetched.add(load.seg.startMs);
24698
24788
  return {
@@ -24704,6 +24794,14 @@ var RecordedFeeder = class {
24704
24794
  return null;
24705
24795
  }
24706
24796
  }
24797
+ /** Yield the spindle to an in-flight finger tick. Returns false if this fill died. */
24798
+ async waitForTickReadIdle(fillGen) {
24799
+ while (this.scrubTickReadInFlight) {
24800
+ await new Promise((r) => setTimeout(r, 4));
24801
+ if (this.disposed || !this.scrubbing || fillGen !== this.scrubWarmGen) return false;
24802
+ }
24803
+ return true;
24804
+ }
24707
24805
  /**
24708
24806
  * EXPERIMENTAL feeder-scrub: register a drag target. Records the newest target
24709
24807
  * and, if no tick cycle is live, starts a leading-edge tick. A burst of moves
@@ -24867,63 +24965,69 @@ var RecordedFeeder = class {
24867
24965
  }
24868
24966
  }
24869
24967
  if (!seg) {
24870
- const locateStartMs = performance.now();
24871
- let loc = await this.withTimeout(this.deps.locate({
24872
- deviceId: this.deps.deviceId,
24873
- profile: this.deps.profile,
24874
- epochMs: target
24875
- }));
24876
- locateCalls += 1;
24877
- locateMs = performance.now() - locateStartMs;
24878
- this.maybeLogSlowLocate(target, locateMs);
24879
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return;
24880
- if (loc.kind === "gap") {
24881
- const decision = resolveSnapTarget(loc, target);
24882
- if (decision.kind !== "snap") {
24883
- if (!this.scrubNoFootageLogged) {
24884
- this.scrubNoFootageLogged = true;
24885
- if (decision.kind === "too-far") this.deps.onLog?.("scrub-feeder:snap-too-far", {
24886
- epochMs: target,
24887
- nearestMs: decision.snapMs,
24888
- distanceMs: decision.distanceMs
24889
- });
24890
- else this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
24891
- }
24892
- return;
24893
- }
24894
- const snapStartMs = performance.now();
24895
- const snapped = await this.withTimeout(this.deps.locate({
24968
+ this.scrubTickReadInFlight = true;
24969
+ try {
24970
+ this.maybeRecenterScrubWarm(target);
24971
+ const locateStartMs = performance.now();
24972
+ let loc = await this.withTimeout(this.deps.locate({
24896
24973
  deviceId: this.deps.deviceId,
24897
24974
  profile: this.deps.profile,
24898
- epochMs: decision.snapMs
24975
+ epochMs: target
24899
24976
  }));
24900
24977
  locateCalls += 1;
24901
- locateMs = (locateMs ?? 0) + (performance.now() - snapStartMs);
24978
+ locateMs = performance.now() - locateStartMs;
24979
+ this.maybeLogSlowLocate(target, locateMs);
24902
24980
  if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return;
24903
- if (snapped.kind !== "segment") {
24904
- if (!this.scrubNoFootageLogged) {
24905
- this.scrubNoFootageLogged = true;
24906
- this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
24981
+ if (loc.kind === "gap") {
24982
+ const decision = resolveSnapTarget(loc, target);
24983
+ if (decision.kind !== "snap") {
24984
+ if (!this.scrubNoFootageLogged) {
24985
+ this.scrubNoFootageLogged = true;
24986
+ if (decision.kind === "too-far") this.deps.onLog?.("scrub-feeder:snap-too-far", {
24987
+ epochMs: target,
24988
+ nearestMs: decision.snapMs,
24989
+ distanceMs: decision.distanceMs
24990
+ });
24991
+ else this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
24992
+ }
24993
+ return;
24907
24994
  }
24908
- return;
24995
+ const snapStartMs = performance.now();
24996
+ const snapped = await this.withTimeout(this.deps.locate({
24997
+ deviceId: this.deps.deviceId,
24998
+ profile: this.deps.profile,
24999
+ epochMs: decision.snapMs
25000
+ }));
25001
+ locateCalls += 1;
25002
+ locateMs = (locateMs ?? 0) + (performance.now() - snapStartMs);
25003
+ if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return;
25004
+ if (snapped.kind !== "segment") {
25005
+ if (!this.scrubNoFootageLogged) {
25006
+ this.scrubNoFootageLogged = true;
25007
+ this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
25008
+ }
25009
+ return;
25010
+ }
25011
+ this.deps.onLog?.("scrub-feeder:snapped", {
25012
+ requestedMs: target,
25013
+ snappedMs: decision.snapMs
25014
+ });
25015
+ loc = snapped;
25016
+ effTarget = decision.snapMs;
24909
25017
  }
24910
- this.deps.onLog?.("scrub-feeder:snapped", {
24911
- requestedMs: target,
24912
- snappedMs: decision.snapMs
24913
- });
24914
- loc = snapped;
24915
- effTarget = decision.snapMs;
24916
- }
24917
- seg = this.scrubCache.covering(this.deps.profile, effTarget);
24918
- if (!seg) {
24919
- const load = await this.loadScrubFragment(loc, effTarget);
24920
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken || load === null) return;
24921
- seg = load.seg;
24922
- readMs = load.readMs;
24923
- readBytes = load.readBytes;
24924
- demuxMs = load.demuxMs;
24925
- wholeSegmentRead = load.wholeSegmentRead;
24926
- this.scrubCache.put(this.deps.profile, seg);
25018
+ seg = this.scrubCache.covering(this.deps.profile, effTarget);
25019
+ if (!seg) {
25020
+ const load = await this.loadScrubFragment(loc, effTarget);
25021
+ if (this.disposed || !this.scrubbing || gen !== this.scrubToken || load === null) return;
25022
+ seg = load.seg;
25023
+ readMs = load.readMs;
25024
+ readBytes = load.readBytes;
25025
+ demuxMs = load.demuxMs;
25026
+ wholeSegmentRead = load.wholeSegmentRead;
25027
+ this.scrubCache.put(this.deps.profile, seg);
25028
+ }
25029
+ } finally {
25030
+ this.scrubTickReadInFlight = false;
24927
25031
  }
24928
25032
  }
24929
25033
  this.scrubSegment = seg;
@@ -25132,7 +25236,8 @@ var RecordedFeeder = class {
25132
25236
  readMs: readEndMs - readStartMs,
25133
25237
  readBytes: bytes.length,
25134
25238
  demuxMs: performance.now() - readEndMs,
25135
- wholeSegmentRead: true
25239
+ wholeSegmentRead: true,
25240
+ rawBytes: bytes
25136
25241
  };
25137
25242
  }
25138
25243
  /**
@@ -25344,6 +25449,7 @@ var RecordedFeeder = class {
25344
25449
  this.scrubStats?.recordPrefetchSuppressed();
25345
25450
  return;
25346
25451
  }
25452
+ if (this.scrubFillLoopActive) return;
25347
25453
  const aheadMs = Math.max(Math.abs(stepMs), seg.durMs + 1);
25348
25454
  const horizons = Math.abs(stepMs) > seg.durMs ? SCRUB_PREFETCH_MAX_INFLIGHT : 1;
25349
25455
  for (let horizon = 1; horizon <= horizons; horizon++) {
@@ -25352,6 +25458,7 @@ var RecordedFeeder = class {
25352
25458
  if (probeMs < 0) continue;
25353
25459
  if (this.scrubPrefetchProbes.has(probeMs)) continue;
25354
25460
  if (this.scrubCache.covering(this.deps.profile, probeMs)) continue;
25461
+ if (this.scrubWarmCentreMs !== null && Math.abs(probeMs - this.scrubWarmCentreMs) > SCRUB_WARM_AROUND_MS) continue;
25355
25462
  this.scrubPrefetch(probeMs, gen);
25356
25463
  }
25357
25464
  }
@@ -25974,7 +26081,7 @@ var TimelineSession = class {
25974
26081
  *
25975
26082
  * 1. The scrub is pinned to `low` by the client, and every D163 pre-cache
25976
26083
  * behaviour is gated on `deps.profile === 'low'` by exact string equality
25977
- * — the 90 s/30 s entry warm, the whole-segment read, and the 24 MB
26084
+ * — the ±15 min entry warm, the whole-segment read, and the 48 MB
25978
26085
  * fragment budget (2 MB for anything else). `profileSearchOrder` puts
25979
26086
  * `high` BEFORE `low`, so a momentary `low` gap used to hand the drag a
25980
26087
  * `high` feeder and silently turn all three off. The client cannot repair
@@ -33710,6 +33817,70 @@ function preferLiveAdaptiveCandidates(candidates) {
33710
33817
  return natives.length > 0 ? natives : [...candidates];
33711
33818
  }
33712
33819
  //#endregion
33820
+ //#region src/stream-broker/webrtc/adaptive-tier.ts
33821
+ var RANK = {
33822
+ low: 1,
33823
+ mid: 2,
33824
+ high: 3
33825
+ };
33826
+ var QUALITY_FIRST = [
33827
+ "high",
33828
+ "mid",
33829
+ "low"
33830
+ ];
33831
+ var SMALLEST_FIRST = [
33832
+ "low",
33833
+ "mid",
33834
+ "high"
33835
+ ];
33836
+ function isTier(value) {
33837
+ return value === "high" || value === "mid" || value === "low";
33838
+ }
33839
+ function firstAvailable(order, available) {
33840
+ const set = new Set(available);
33841
+ return order.find((t) => set.has(t));
33842
+ }
33843
+ function capTo(tier, available) {
33844
+ const max = RANK[tier];
33845
+ return firstAvailable(QUALITY_FIRST.filter((t) => RANK[t] <= max), available);
33846
+ }
33847
+ function pickAdaptiveTier(input) {
33848
+ const available = input.available.filter((t) => t === "high" || t === "mid" || t === "low");
33849
+ const fallback = firstAvailable(QUALITY_FIRST, available) ?? "high";
33850
+ if (isTier(input.prefersTier)) {
33851
+ const exact = available.includes(input.prefersTier) ? input.prefersTier : void 0;
33852
+ if (exact) return {
33853
+ tier: exact,
33854
+ reason: "prefers-tier"
33855
+ };
33856
+ return {
33857
+ tier: firstAvailable(SMALLEST_FIRST, available) ?? fallback,
33858
+ reason: "prefers-tier-fallback"
33859
+ };
33860
+ }
33861
+ let tier = fallback;
33862
+ let reason = "default-high";
33863
+ if (input.downlinkMbps !== void 0 && Number.isFinite(input.downlinkMbps) && input.downlinkMbps >= 0) {
33864
+ tier = capTo(input.downlinkMbps >= 8 ? "high" : input.downlinkMbps >= 2.5 ? "mid" : "low", available) ?? fallback;
33865
+ reason = "downlink";
33866
+ }
33867
+ const width = input.viewportWidth;
33868
+ const height = input.viewportHeight;
33869
+ if (width !== void 0 && height !== void 0 && width > 0 && height > 0) {
33870
+ if (Math.max(width, height) < 240) {
33871
+ const tiled = capTo("low", available) ?? fallback;
33872
+ if (RANK[tiled] < RANK[tier]) return {
33873
+ tier: tiled,
33874
+ reason: "viewport-ceiling"
33875
+ };
33876
+ }
33877
+ }
33878
+ return {
33879
+ tier,
33880
+ reason
33881
+ };
33882
+ }
33883
+ //#endregion
33713
33884
  //#region src/stream-broker/webrtc/broker-webrtc-server.ts
33714
33885
  /**
33715
33886
  * Broker-integrated WebRTC server.
@@ -33825,36 +33996,12 @@ function deriveH264ProfileLevelId(sdpParameterSets, preBuffer) {
33825
33996
  if (profileLevelId) return profileLevelId;
33826
33997
  }
33827
33998
  }
33828
- /** Fallback resolution/bitrate per label when the broker hasn't reported stats yet. */
33829
- var LABEL_DEFAULTS = {
33830
- high: {
33831
- pixels: 1920 * 1080,
33832
- bitrateKbps: 4e3
33833
- },
33834
- mid: {
33835
- pixels: 1280 * 720,
33836
- bitrateKbps: 1200
33837
- },
33838
- low: {
33839
- pixels: 640 * 360,
33840
- bitrateKbps: 400
33841
- }
33842
- };
33843
- var TIER_PREFERENCE = [
33844
- "high",
33845
- "mid",
33846
- "low"
33847
- ];
33848
- /** Smallest-keyframe-first tier order. Used as the fallback when an explicit
33849
- * `prefersTier` (e.g. the hub's `remote`-viewer bias to `low`) has no exact
33850
- * assigned slot: prefer the SMALLEST available tier rather than the
33851
- * quality-first default, so a remote-biased request never silently lands back
33852
- * on `high`. */
33853
- var SMALLEST_FIRST_TIER_PREFERENCE = [
33854
- "low",
33855
- "mid",
33856
- "high"
33857
- ];
33999
+ function adaptivePickLogLine(reason) {
34000
+ if (reason === "prefers-tier") return "Adaptive source selected (prefersTier exact)";
34001
+ if (reason === "prefers-tier-fallback") return "Adaptive source selected (prefersTier fallback → smallest available)";
34002
+ if (reason === "default-high") return "Adaptive source selected (no client hints — tier preference)";
34003
+ return "Adaptive source selected";
34004
+ }
33858
34005
  /** Push-queue overflow purges are aggregated and reported at most this often. */
33859
34006
  var FRAME_DROP_LOG_INTERVAL_MS = 3e4;
33860
34007
  /**
@@ -33895,20 +34042,6 @@ function reduceProfileForTranscodeDown(base) {
33895
34042
  }
33896
34043
  };
33897
34044
  }
33898
- function scoreBroker(tier, broker, hints) {
33899
- const stats = broker.getStats();
33900
- const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
33901
- const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
33902
- const srcPixels = fallback?.pixels ?? 1920 * 1080;
33903
- const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
33904
- const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
33905
- let bandwidthPenalty = 0;
33906
- if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
33907
- const availableKbps = hints.downlinkMbps * 1e3 * .6;
33908
- if (bitrateKbps > availableKbps) bandwidthPenalty = (bitrateKbps - availableKbps) / availableKbps * 100;
33909
- }
33910
- return pixelDistance + bandwidthPenalty;
33911
- }
33912
34045
  var BrokerWebrtcServer = class {
33913
34046
  logger;
33914
34047
  getIceServersFn;
@@ -35040,78 +35173,26 @@ var BrokerWebrtcServer = class {
35040
35173
  if (pool.length === 1) return pool[0].brokerId;
35041
35174
  const adaptiveDeviceIdTag = Number(deviceKey.split("/")[0]);
35042
35175
  const deviceIdTag = Number.isFinite(adaptiveDeviceIdTag) ? { tags: { deviceId: adaptiveDeviceIdTag } } : {};
35043
- if (hints.prefersTier === "high" || hints.prefersTier === "mid" || hints.prefersTier === "low") {
35044
- const explicit = pool.find((c) => c.tier === hints.prefersTier);
35045
- if (explicit) {
35046
- this.logger.info("Adaptive source selected (prefersTier exact)", {
35047
- ...deviceIdTag,
35048
- meta: {
35049
- deviceKey,
35050
- selected: explicit.brokerId,
35051
- tier: explicit.tier,
35052
- prefersTier: hints.prefersTier
35053
- }
35054
- });
35055
- return explicit.brokerId;
35056
- }
35057
- for (const tier of SMALLEST_FIRST_TIER_PREFERENCE) {
35058
- const match = pool.find((c) => c.tier === tier);
35059
- if (match) {
35060
- this.logger.info("Adaptive source selected (prefersTier fallback → smallest available)", {
35061
- ...deviceIdTag,
35062
- meta: {
35063
- deviceKey,
35064
- selected: match.brokerId,
35065
- tier: match.tier,
35066
- prefersTier: hints.prefersTier
35067
- }
35068
- });
35069
- return match.brokerId;
35070
- }
35071
- }
35072
- }
35073
- if (!(hints.viewportWidth !== void 0 || hints.viewportHeight !== void 0 || hints.downlinkMbps !== void 0)) {
35074
- for (const tier of TIER_PREFERENCE) {
35075
- const match = pool.find((c) => c.tier === tier);
35076
- if (match) {
35077
- this.logger.info("Adaptive source selected (no client hints — tier preference)", {
35078
- ...deviceIdTag,
35079
- meta: {
35080
- deviceKey,
35081
- selected: match.brokerId,
35082
- tier: match.tier
35083
- }
35084
- });
35085
- return match.brokerId;
35086
- }
35087
- }
35088
- return pool[0].brokerId;
35089
- }
35090
- let bestId = pool[0].brokerId;
35091
- let bestTier = pool[0].tier;
35092
- let bestScore = Infinity;
35093
- for (const c of pool) {
35094
- const score = scoreBroker(c.tier, c.broker, hints);
35095
- const cRank = c.tier ? TIER_PREFERENCE.indexOf(c.tier) : TIER_PREFERENCE.length;
35096
- const bestRank = bestTier ? TIER_PREFERENCE.indexOf(bestTier) : TIER_PREFERENCE.length;
35097
- if (score < bestScore || score === bestScore && cRank < bestRank) {
35098
- bestId = c.brokerId;
35099
- bestTier = c.tier;
35100
- bestScore = score;
35101
- }
35102
- }
35103
- const adaptiveDeviceId = Number(deviceKey.split("/")[0]);
35104
- this.logger.debug("Adaptive source selected", {
35105
- ...Number.isFinite(adaptiveDeviceId) ? { tags: { deviceId: adaptiveDeviceId } } : {},
35176
+ const pick = pickAdaptiveTier({
35177
+ available: pool.map((c) => c.tier),
35178
+ prefersTier: hints.prefersTier,
35179
+ downlinkMbps: hints.downlinkMbps,
35180
+ viewportWidth: hints.viewportWidth,
35181
+ viewportHeight: hints.viewportHeight
35182
+ });
35183
+ const selected = pool.find((c) => c.tier === pick.tier) ?? pool[0];
35184
+ this.logger.info(adaptivePickLogLine(pick.reason), {
35185
+ ...deviceIdTag,
35106
35186
  meta: {
35107
35187
  deviceKey,
35108
- selected: bestId,
35109
- tier: bestTier,
35110
- score: Math.round(bestScore * 100) / 100,
35188
+ selected: selected.brokerId,
35189
+ tier: selected.tier,
35190
+ reason: pick.reason,
35191
+ prefersTier: hints.prefersTier,
35111
35192
  hints
35112
35193
  }
35113
35194
  });
35114
- return bestId;
35195
+ return selected.brokerId;
35115
35196
  }
35116
35197
  async resolveIceServers() {
35117
35198
  if (this.getIceServersFn) try {