@camstack/addon-pipeline 1.2.106 → 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-BEzVJ0xg.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 +198 -38
  6. package/dist/detection-pipeline/index.mjs +197 -37
  7. package/dist/{dist-BSeR_hVv.js → dist-BqIbYW1A.js} +290 -17
  8. package/dist/{dist-lITI1Bn5.mjs → dist-DGQkkWc6.mjs} +273 -18
  9. package/dist/{event-loop-stall-monitor-0VvjTRDP.mjs → event-loop-stall-monitor-D5jK5v4Z.mjs} +223 -2
  10. package/dist/{event-loop-stall-monitor-BNsp9k8r.js → event-loop-stall-monitor-ylMGpz48.js} +234 -1
  11. package/dist/{lazy-sharp-CBohM8D8.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-C33u3tQz.js → process-memory-45juuHpN.js} +1 -1
  17. package/dist/{process-memory-CEKVKfwg.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-w2XLD3uI.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-DsIi4G5Q.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-B-h24cDy.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-CiKfal2P.js → worker-protocol-CIrqGbpG.js} +6 -3
  31. package/dist/{worker-protocol-BDiL8TBm.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-DvZWLWk1.mjs +0 -26
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "../chunk-DnnnRqeS.mjs";
2
- import { Bt as record, D as addonWidgetsSourceCapability, Dt as nodePin, Et as makeSourceBrokerId, Ft as discriminatedUnion, H as egressTransportFromRequest, Ht as union, L as deriveBatteryPresence, Lt as literal, M as cameraStreamsCapability, Mt as _enum, N as createHwAccelCache, Nt as array, Pt as boolean, Rt as number, S as RATE_CONTROL_TIGHT, St as createEvent, T as RingBuffer, Tt as makeProfileBrokerId, Ut as EventCategory, V as egressTranscodeSharingKey, Vt as string, _t as isSoftwareDecode, bt as DeviceFeature, dt as webrtcSessionCapability, ft as errMsg, gt as invocationFromEncodeProfile, ht as buildFfmpegArgs$1, i as BatteryStatusSchema, it as resolveEgressDecodeHwAccel, j as batteryCapability, kt as parseProfileBrokerId, lt as streamBrokerCapability, m as EncodeProfileSchema, mt as Fmp4BoxSplitter, pt as AUDIO_PRESETS, q as maskUrlCredentials, vt as BaseAddon, wt as isEvent, x as RATE_CONTROL_RELAXED, xt as DeviceType, yt as CAM_PROFILE_ORDER, zt as object } from "../dist-lITI1Bn5.mjs";
2
+ import { At as nodePin, Bt as literal, C as RATE_CONTROL_TIGHT, Ct as DeviceFeature, Dt as isEvent, E as RingBuffer, Ft as _enum, Gt as union, H as egressTranscodeSharingKey, Ht as object, It as array, J as maskUrlCredentials, Kt as EventCategory, Lt as boolean, M as batteryCapability, Mt as parseProfileBrokerId, N as cameraStreamsCapability, O as addonWidgetsSourceCapability, Ot as makeProfileBrokerId, P as createHwAccelCache, R as deriveBatteryPresence, Rt as discriminatedUnion, S as RATE_CONTROL_RELAXED, St as CAM_PROFILE_ORDER, Tt as createEvent, U as egressTransportFromRequest, Ut as record, Vt as number, Wt as string, _t as Fmp4BoxSplitter, bt as isSoftwareDecode, ft as streamBrokerCapability, gt as AUDIO_PRESETS, h as EncodeProfileSchema, ht as errMsg, i as BatteryStatusSchema, kt as makeSourceBrokerId, mt as webrtcSessionCapability, st as resolveEgressDecodeHwAccel, vt as buildFfmpegArgs$1, wt as DeviceType, xt as BaseAddon, yt as invocationFromEncodeProfile } from "../dist-DGQkkWc6.mjs";
3
3
  import { t as parseRestreamPath } from "../restream-intent-B4BXZra7.mjs";
4
4
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
5
5
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DMolfhsP.mjs";
@@ -23626,7 +23626,7 @@ var MonotonicClock = class {
23626
23626
  this.rate = rate;
23627
23627
  }
23628
23628
  };
23629
- var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 24 * 1024 * 1024;
23629
+ var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 48 * 1024 * 1024;
23630
23630
  var SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES = 2 * 1024 * 1024;
23631
23631
  /** Demuxed bytes an entry holds — what the byte bound actually counts. */
23632
23632
  function fragmentBytes(frag) {
@@ -24048,14 +24048,22 @@ var SCRUB_WHOLE_SEGMENT_MAX_BYTES = 15e5;
24048
24048
  * drag starts on.
24049
24049
  *
24050
24050
  * Asymmetric because the gesture is: a drag that begins at the live edge — or
24051
- * at a notification's moment — is looking for what ALREADY happened. 90 s back
24052
- * against 30 s forward is 12 segments ≈ 2.8 MB at `low`, read sequentially in
24053
- * the background while the operator is still deciding where to drag.
24051
+ * at a notification's moment — is looking for what ALREADY happened. 15 min
24052
+ * back against 5 min forward fills the 48 MB window where the finger will
24053
+ * actually move, read sequentially in the background while the first ticks
24054
+ * still pay their own demand-read.
24054
24055
  */
24055
- var SCRUB_WARM_BACK_MS = 9e4;
24056
- var SCRUB_WARM_FWD_MS = 3e4;
24057
- /** Hard bound on warm reads, whatever the spans above resolve to. */
24058
- var SCRUB_WARM_MAX_SEGMENTS = 14;
24056
+ var SCRUB_WARM_BACK_MS = 15 * 6e4;
24057
+ var SCRUB_WARM_FWD_MS = 5 * 6e4;
24058
+ /** Symmetric span used after a jump: scrub around the landing, both ways. */
24059
+ var SCRUB_WARM_AROUND_MS = 15 * 6e4;
24060
+ /**
24061
+ * Re-centre the fill when a tick lands this far from the window we are filling.
24062
+ * A slow drag stays inside this; a jump to 8am does not.
24063
+ */
24064
+ var SCRUB_RECENTER_MS = 12e4;
24065
+ /** Hard bound on warm reads, derived from the session RAM budget. */
24066
+ var SCRUB_WARM_MAX_SEGMENTS = Math.max(1, Math.floor(SCRUB_FRAGMENT_CACHE_MAX_BYTES / 2e5));
24059
24067
  /**
24060
24068
  * Median recent read time at or above which the feeder STOPS speculating.
24061
24069
  *
@@ -24309,6 +24317,16 @@ var RecordedFeeder = class {
24309
24317
  scrubReadMsWindow = [];
24310
24318
  /** Warm-up runs once per drag, from the playhead the finger went down on. */
24311
24319
  scrubWarmStarted = false;
24320
+ /** Centre of the fill currently in flight (or last completed). */
24321
+ scrubWarmCentreMs = null;
24322
+ /** Bumped to abort a fill without aborting the drag tick (`scrubToken`). */
24323
+ scrubWarmGen = 0;
24324
+ /** Tick demand-read in flight: the fill waits so the finger wins the spindle. */
24325
+ scrubTickReadInFlight = false;
24326
+ /** One fill walker per drag — a fling retargets it, it does not spawn another. */
24327
+ scrubFillLoopActive = false;
24328
+ scrubWarmBackMs = SCRUB_WARM_BACK_MS;
24329
+ scrubWarmFwdMs = SCRUB_WARM_FWD_MS;
24312
24330
  constructor(deps) {
24313
24331
  this.deps = deps;
24314
24332
  this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3);
@@ -24430,6 +24448,7 @@ var RecordedFeeder = class {
24430
24448
  let prefixFed = false;
24431
24449
  let gopFirst = false;
24432
24450
  let stateAnnounced = false;
24451
+ let reusedWhole = null;
24433
24452
  if (this.deps.readGop) {
24434
24453
  const cached = this.scrubCache.covering(this.deps.profile, landMs);
24435
24454
  let fragment;
@@ -24439,6 +24458,10 @@ var RecordedFeeder = class {
24439
24458
  if (fragmentLoad === null) return;
24440
24459
  fragment = fragmentLoad.seg;
24441
24460
  this.scrubCache.put(this.deps.profile, fragment);
24461
+ if (fragmentLoad.wholeSegmentRead && fragmentLoad.rawBytes) reusedWhole = {
24462
+ video: fragment.aus,
24463
+ bytes: fragmentLoad.rawBytes
24464
+ };
24442
24465
  }
24443
24466
  if (this.disposed || token !== this.seekToken) return;
24444
24467
  const fragmentLoc = {
@@ -24456,14 +24479,23 @@ var RecordedFeeder = class {
24456
24479
  this.startPositionReports();
24457
24480
  stateAnnounced = true;
24458
24481
  }
24459
- const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
24460
- if (this.disposed || token !== this.seekToken) return;
24461
- this.feedPrefix(loc, video, landMs);
24462
- prefixFed = true;
24463
- firstFrameMs = performance.now() - seekStartedAt;
24464
- });
24465
- fullLoad.catch(() => {});
24466
- const loaded = await fullLoad;
24482
+ let loaded;
24483
+ if (reusedWhole) {
24484
+ const audio = this.rate !== 0 && this.deps.demuxAudio && this.resolveAudioPush() !== void 0 ? await this.withTimeout(this.deps.demuxAudio(reusedWhole.bytes)).catch(() => []) : [];
24485
+ loaded = {
24486
+ video: reusedWhole.video,
24487
+ audio
24488
+ };
24489
+ } else {
24490
+ const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
24491
+ if (this.disposed || token !== this.seekToken) return;
24492
+ this.feedPrefix(loc, video, landMs);
24493
+ prefixFed = true;
24494
+ firstFrameMs = performance.now() - seekStartedAt;
24495
+ });
24496
+ fullLoad.catch(() => {});
24497
+ loaded = await fullLoad;
24498
+ }
24467
24499
  loadMs = performance.now() - loadStartedAt;
24468
24500
  if (this.disposed || token !== this.seekToken || loaded === null) return;
24469
24501
  if (!prefixFed) this.feedPrefix(loc, loaded.video, landMs);
@@ -24625,38 +24657,92 @@ var RecordedFeeder = class {
24625
24657
  * back and {@link SCRUB_WARM_FWD_MS} forward from the entry playhead, one
24626
24658
  * segment at a time, backward first.
24627
24659
  *
24628
- * Once per drag, always in the background, and never on the tick's path — a
24629
- * tick that arrives against a cold cache pays its own read exactly as before.
24660
+ * Fire-and-forget, never on the tick's path — a tick that arrives against a
24661
+ * cold cache pays its own read exactly as before. A later jump more than
24662
+ * {@link SCRUB_RECENTER_MS} from this centre aborts the fill (via
24663
+ * {@link scrubWarmGen}, not the drag token) and starts a new window.
24664
+ *
24630
24665
  * It walks segment to segment through `locate` rather than assuming a segment
24631
24666
  * length, so a recorder configured with a different `segmentSeconds` warms the
24632
24667
  * same SPAN rather than the same count.
24633
24668
  */
24634
- startScrubWarm(centreMs, gen) {
24635
- if (this.scrubWarmStarted) return;
24669
+ startScrubWarm(centreMs, dragGen, opts) {
24670
+ if (this.scrubWarmStarted && !opts?.force) return;
24636
24671
  this.scrubWarmStarted = true;
24637
- this.warmScrubWindow(centreMs, gen);
24672
+ this.scrubWarmGen += 1;
24673
+ this.scrubWarmCentreMs = centreMs;
24674
+ this.scrubWarmBackMs = opts?.backMs ?? SCRUB_WARM_BACK_MS;
24675
+ this.scrubWarmFwdMs = opts?.fwdMs ?? SCRUB_WARM_FWD_MS;
24676
+ this.deps.onLog?.("scrub-feeder:warm-start", {
24677
+ centreMs,
24678
+ fillGen: this.scrubWarmGen
24679
+ });
24680
+ if (this.scrubFillLoopActive) return;
24681
+ this.scrubFillLoopActive = true;
24682
+ this.runScrubFillLoop(dragGen);
24683
+ }
24684
+ /**
24685
+ * A single sequential walk. Recentre bumps {@link scrubWarmGen} and this loop
24686
+ * restarts around the new cursor — it must never `void` a second walker, or a
24687
+ * coarse fling stacks one USB walk per tick and play/scrub starve (1.2.113:
24688
+ * 16 walkers, seek `loadMs=16652`, drags with `ticks=0`).
24689
+ */
24690
+ async runScrubFillLoop(dragGen) {
24691
+ let lastWorked = -1;
24692
+ try {
24693
+ while (!this.disposed && this.scrubbing && dragGen === this.scrubToken) {
24694
+ const gen = this.scrubWarmGen;
24695
+ const centre = this.scrubWarmCentreMs;
24696
+ if (centre === null) return;
24697
+ await this.warmScrubWindow(centre, dragGen, gen, this.scrubWarmBackMs, this.scrubWarmFwdMs);
24698
+ lastWorked = gen;
24699
+ if (gen === this.scrubWarmGen) return;
24700
+ }
24701
+ } finally {
24702
+ this.scrubFillLoopActive = false;
24703
+ if (!this.disposed && this.scrubbing && dragGen === this.scrubToken && this.scrubWarmCentreMs !== null && this.scrubWarmGen !== lastWorked) {
24704
+ this.scrubFillLoopActive = true;
24705
+ this.runScrubFillLoop(dragGen);
24706
+ }
24707
+ }
24708
+ }
24709
+ /**
24710
+ * Abort the current fill and start a new ±15 min window around `centreMs`.
24711
+ * Does not bump `scrubToken` — the in-flight tick must still land.
24712
+ */
24713
+ recenterScrubWarm(centreMs) {
24714
+ if (this.deps.profile !== SCRUB_WHOLE_SEGMENT_PROFILE) return;
24715
+ this.startScrubWarm(centreMs, this.scrubToken, {
24716
+ force: true,
24717
+ backMs: SCRUB_WARM_AROUND_MS,
24718
+ fwdMs: SCRUB_WARM_AROUND_MS
24719
+ });
24720
+ }
24721
+ maybeRecenterScrubWarm(targetMs) {
24722
+ if (this.deps.profile !== SCRUB_WHOLE_SEGMENT_PROFILE) return;
24723
+ const centre = this.scrubWarmCentreMs;
24724
+ if (centre === null) return;
24725
+ if (Math.abs(targetMs - centre) <= SCRUB_RECENTER_MS) return;
24726
+ this.recenterScrubWarm(targetMs);
24638
24727
  }
24639
- async warmScrubWindow(centreMs, gen) {
24640
- const spent = await this.warmScrubDirection(centreMs, -1, SCRUB_WARM_BACK_MS, 0, gen);
24641
- await this.warmScrubDirection(centreMs, 1, SCRUB_WARM_FWD_MS, spent, gen);
24728
+ async warmScrubWindow(centreMs, dragGen, fillGen, backMs, fwdMs) {
24729
+ const spent = await this.warmScrubDirection(centreMs, -1, backMs, 0, dragGen, fillGen);
24730
+ await this.warmScrubDirection(centreMs, 1, fwdMs, spent, dragGen, fillGen);
24642
24731
  }
24643
24732
  /**
24644
24733
  * Walk `dir` from `fromMs` until `spanMs` of timeline is warm (or the shared
24645
24734
  * bound is hit), reading each segment that is not already cached. Returns the
24646
24735
  * running total of segments read, so the two directions share one budget.
24647
24736
  */
24648
- async warmScrubDirection(fromMs, dir, spanMs, spent, gen) {
24737
+ async warmScrubDirection(fromMs, dir, spanMs, spent, dragGen, fillGen) {
24649
24738
  let read = spent;
24650
24739
  let probeMs = fromMs;
24651
24740
  for (let i = 0; i < SCRUB_WARM_MAX_SEGMENTS; i++) {
24652
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return read;
24741
+ if (this.disposed || !this.scrubbing || dragGen !== this.scrubToken) return read;
24742
+ if (fillGen !== this.scrubWarmGen) return read;
24653
24743
  if (read >= SCRUB_WARM_MAX_SEGMENTS) return read;
24654
24744
  if (Math.abs(probeMs - fromMs) > spanMs) return read;
24655
- if (this.scrubReadsAreSlow()) {
24656
- this.scrubStats?.recordPrefetchSuppressed();
24657
- return read;
24658
- }
24659
- const seg = await this.warmOneScrubSegment(probeMs, gen);
24745
+ const seg = await this.warmOneScrubSegment(probeMs, dragGen, fillGen);
24660
24746
  if (seg === null) return read;
24661
24747
  if (seg.fetched) read += 1;
24662
24748
  probeMs = dir === -1 ? seg.startMs - 1 : seg.startMs + seg.durMs + 1;
@@ -24670,7 +24756,7 @@ var RecordedFeeder = class {
24670
24756
  * so the walk can step to its neighbour, or null when the walk must stop (a
24671
24757
  * gap, a failure, or teardown).
24672
24758
  */
24673
- async warmOneScrubSegment(probeMs, gen) {
24759
+ async warmOneScrubSegment(probeMs, dragGen, fillGen) {
24674
24760
  try {
24675
24761
  const warm = this.scrubCache.covering(this.deps.profile, probeMs);
24676
24762
  if (warm) return {
@@ -24678,16 +24764,20 @@ var RecordedFeeder = class {
24678
24764
  durMs: warm.durMs,
24679
24765
  fetched: false
24680
24766
  };
24767
+ if (!await this.waitForTickReadIdle(fillGen)) return null;
24681
24768
  const loc = await this.withTimeout(this.deps.locate({
24682
24769
  deviceId: this.deps.deviceId,
24683
24770
  profile: this.deps.profile,
24684
24771
  epochMs: probeMs
24685
24772
  }));
24686
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return null;
24773
+ if (this.disposed || !this.scrubbing || dragGen !== this.scrubToken) return null;
24774
+ if (fillGen !== this.scrubWarmGen) return null;
24687
24775
  if (loc.kind !== "segment") return null;
24776
+ if (!await this.waitForTickReadIdle(fillGen)) return null;
24688
24777
  const load = await this.loadScrubFragment(loc, probeMs);
24689
24778
  if (load !== null) this.scrubStats?.recordPrefetchRead(load.readBytes);
24690
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken || load === null) return null;
24779
+ if (this.disposed || !this.scrubbing || dragGen !== this.scrubToken || load === null) return null;
24780
+ if (fillGen !== this.scrubWarmGen) return null;
24691
24781
  this.scrubCache.put(this.deps.profile, load.seg);
24692
24782
  this.scrubPrefetched.add(load.seg.startMs);
24693
24783
  return {
@@ -24699,6 +24789,14 @@ var RecordedFeeder = class {
24699
24789
  return null;
24700
24790
  }
24701
24791
  }
24792
+ /** Yield the spindle to an in-flight finger tick. Returns false if this fill died. */
24793
+ async waitForTickReadIdle(fillGen) {
24794
+ while (this.scrubTickReadInFlight) {
24795
+ await new Promise((r) => setTimeout(r, 4));
24796
+ if (this.disposed || !this.scrubbing || fillGen !== this.scrubWarmGen) return false;
24797
+ }
24798
+ return true;
24799
+ }
24702
24800
  /**
24703
24801
  * EXPERIMENTAL feeder-scrub: register a drag target. Records the newest target
24704
24802
  * and, if no tick cycle is live, starts a leading-edge tick. A burst of moves
@@ -24862,63 +24960,69 @@ var RecordedFeeder = class {
24862
24960
  }
24863
24961
  }
24864
24962
  if (!seg) {
24865
- const locateStartMs = performance.now();
24866
- let loc = await this.withTimeout(this.deps.locate({
24867
- deviceId: this.deps.deviceId,
24868
- profile: this.deps.profile,
24869
- epochMs: target
24870
- }));
24871
- locateCalls += 1;
24872
- locateMs = performance.now() - locateStartMs;
24873
- this.maybeLogSlowLocate(target, locateMs);
24874
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return;
24875
- if (loc.kind === "gap") {
24876
- const decision = resolveSnapTarget(loc, target);
24877
- if (decision.kind !== "snap") {
24878
- if (!this.scrubNoFootageLogged) {
24879
- this.scrubNoFootageLogged = true;
24880
- if (decision.kind === "too-far") this.deps.onLog?.("scrub-feeder:snap-too-far", {
24881
- epochMs: target,
24882
- nearestMs: decision.snapMs,
24883
- distanceMs: decision.distanceMs
24884
- });
24885
- else this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
24886
- }
24887
- return;
24888
- }
24889
- const snapStartMs = performance.now();
24890
- const snapped = await this.withTimeout(this.deps.locate({
24963
+ this.scrubTickReadInFlight = true;
24964
+ try {
24965
+ this.maybeRecenterScrubWarm(target);
24966
+ const locateStartMs = performance.now();
24967
+ let loc = await this.withTimeout(this.deps.locate({
24891
24968
  deviceId: this.deps.deviceId,
24892
24969
  profile: this.deps.profile,
24893
- epochMs: decision.snapMs
24970
+ epochMs: target
24894
24971
  }));
24895
24972
  locateCalls += 1;
24896
- locateMs = (locateMs ?? 0) + (performance.now() - snapStartMs);
24973
+ locateMs = performance.now() - locateStartMs;
24974
+ this.maybeLogSlowLocate(target, locateMs);
24897
24975
  if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return;
24898
- if (snapped.kind !== "segment") {
24899
- if (!this.scrubNoFootageLogged) {
24900
- this.scrubNoFootageLogged = true;
24901
- this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
24976
+ if (loc.kind === "gap") {
24977
+ const decision = resolveSnapTarget(loc, target);
24978
+ if (decision.kind !== "snap") {
24979
+ if (!this.scrubNoFootageLogged) {
24980
+ this.scrubNoFootageLogged = true;
24981
+ if (decision.kind === "too-far") this.deps.onLog?.("scrub-feeder:snap-too-far", {
24982
+ epochMs: target,
24983
+ nearestMs: decision.snapMs,
24984
+ distanceMs: decision.distanceMs
24985
+ });
24986
+ else this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
24987
+ }
24988
+ return;
24902
24989
  }
24903
- return;
24990
+ const snapStartMs = performance.now();
24991
+ const snapped = await this.withTimeout(this.deps.locate({
24992
+ deviceId: this.deps.deviceId,
24993
+ profile: this.deps.profile,
24994
+ epochMs: decision.snapMs
24995
+ }));
24996
+ locateCalls += 1;
24997
+ locateMs = (locateMs ?? 0) + (performance.now() - snapStartMs);
24998
+ if (this.disposed || !this.scrubbing || gen !== this.scrubToken) return;
24999
+ if (snapped.kind !== "segment") {
25000
+ if (!this.scrubNoFootageLogged) {
25001
+ this.scrubNoFootageLogged = true;
25002
+ this.deps.onLog?.("scrub-feeder:no-footage", { epochMs: target });
25003
+ }
25004
+ return;
25005
+ }
25006
+ this.deps.onLog?.("scrub-feeder:snapped", {
25007
+ requestedMs: target,
25008
+ snappedMs: decision.snapMs
25009
+ });
25010
+ loc = snapped;
25011
+ effTarget = decision.snapMs;
24904
25012
  }
24905
- this.deps.onLog?.("scrub-feeder:snapped", {
24906
- requestedMs: target,
24907
- snappedMs: decision.snapMs
24908
- });
24909
- loc = snapped;
24910
- effTarget = decision.snapMs;
24911
- }
24912
- seg = this.scrubCache.covering(this.deps.profile, effTarget);
24913
- if (!seg) {
24914
- const load = await this.loadScrubFragment(loc, effTarget);
24915
- if (this.disposed || !this.scrubbing || gen !== this.scrubToken || load === null) return;
24916
- seg = load.seg;
24917
- readMs = load.readMs;
24918
- readBytes = load.readBytes;
24919
- demuxMs = load.demuxMs;
24920
- wholeSegmentRead = load.wholeSegmentRead;
24921
- this.scrubCache.put(this.deps.profile, seg);
25013
+ seg = this.scrubCache.covering(this.deps.profile, effTarget);
25014
+ if (!seg) {
25015
+ const load = await this.loadScrubFragment(loc, effTarget);
25016
+ if (this.disposed || !this.scrubbing || gen !== this.scrubToken || load === null) return;
25017
+ seg = load.seg;
25018
+ readMs = load.readMs;
25019
+ readBytes = load.readBytes;
25020
+ demuxMs = load.demuxMs;
25021
+ wholeSegmentRead = load.wholeSegmentRead;
25022
+ this.scrubCache.put(this.deps.profile, seg);
25023
+ }
25024
+ } finally {
25025
+ this.scrubTickReadInFlight = false;
24922
25026
  }
24923
25027
  }
24924
25028
  this.scrubSegment = seg;
@@ -25127,7 +25231,8 @@ var RecordedFeeder = class {
25127
25231
  readMs: readEndMs - readStartMs,
25128
25232
  readBytes: bytes.length,
25129
25233
  demuxMs: performance.now() - readEndMs,
25130
- wholeSegmentRead: true
25234
+ wholeSegmentRead: true,
25235
+ rawBytes: bytes
25131
25236
  };
25132
25237
  }
25133
25238
  /**
@@ -25339,6 +25444,7 @@ var RecordedFeeder = class {
25339
25444
  this.scrubStats?.recordPrefetchSuppressed();
25340
25445
  return;
25341
25446
  }
25447
+ if (this.scrubFillLoopActive) return;
25342
25448
  const aheadMs = Math.max(Math.abs(stepMs), seg.durMs + 1);
25343
25449
  const horizons = Math.abs(stepMs) > seg.durMs ? SCRUB_PREFETCH_MAX_INFLIGHT : 1;
25344
25450
  for (let horizon = 1; horizon <= horizons; horizon++) {
@@ -25347,6 +25453,7 @@ var RecordedFeeder = class {
25347
25453
  if (probeMs < 0) continue;
25348
25454
  if (this.scrubPrefetchProbes.has(probeMs)) continue;
25349
25455
  if (this.scrubCache.covering(this.deps.profile, probeMs)) continue;
25456
+ if (this.scrubWarmCentreMs !== null && Math.abs(probeMs - this.scrubWarmCentreMs) > SCRUB_WARM_AROUND_MS) continue;
25350
25457
  this.scrubPrefetch(probeMs, gen);
25351
25458
  }
25352
25459
  }
@@ -25969,7 +26076,7 @@ var TimelineSession = class {
25969
26076
  *
25970
26077
  * 1. The scrub is pinned to `low` by the client, and every D163 pre-cache
25971
26078
  * behaviour is gated on `deps.profile === 'low'` by exact string equality
25972
- * — the 90 s/30 s entry warm, the whole-segment read, and the 24 MB
26079
+ * — the ±15 min entry warm, the whole-segment read, and the 48 MB
25973
26080
  * fragment budget (2 MB for anything else). `profileSearchOrder` puts
25974
26081
  * `high` BEFORE `low`, so a momentary `low` gap used to hand the drag a
25975
26082
  * `high` feeder and silently turn all three off. The client cannot repair
@@ -33702,6 +33809,70 @@ function preferLiveAdaptiveCandidates(candidates) {
33702
33809
  return natives.length > 0 ? natives : [...candidates];
33703
33810
  }
33704
33811
  //#endregion
33812
+ //#region src/stream-broker/webrtc/adaptive-tier.ts
33813
+ var RANK = {
33814
+ low: 1,
33815
+ mid: 2,
33816
+ high: 3
33817
+ };
33818
+ var QUALITY_FIRST = [
33819
+ "high",
33820
+ "mid",
33821
+ "low"
33822
+ ];
33823
+ var SMALLEST_FIRST = [
33824
+ "low",
33825
+ "mid",
33826
+ "high"
33827
+ ];
33828
+ function isTier(value) {
33829
+ return value === "high" || value === "mid" || value === "low";
33830
+ }
33831
+ function firstAvailable(order, available) {
33832
+ const set = new Set(available);
33833
+ return order.find((t) => set.has(t));
33834
+ }
33835
+ function capTo(tier, available) {
33836
+ const max = RANK[tier];
33837
+ return firstAvailable(QUALITY_FIRST.filter((t) => RANK[t] <= max), available);
33838
+ }
33839
+ function pickAdaptiveTier(input) {
33840
+ const available = input.available.filter((t) => t === "high" || t === "mid" || t === "low");
33841
+ const fallback = firstAvailable(QUALITY_FIRST, available) ?? "high";
33842
+ if (isTier(input.prefersTier)) {
33843
+ const exact = available.includes(input.prefersTier) ? input.prefersTier : void 0;
33844
+ if (exact) return {
33845
+ tier: exact,
33846
+ reason: "prefers-tier"
33847
+ };
33848
+ return {
33849
+ tier: firstAvailable(SMALLEST_FIRST, available) ?? fallback,
33850
+ reason: "prefers-tier-fallback"
33851
+ };
33852
+ }
33853
+ let tier = fallback;
33854
+ let reason = "default-high";
33855
+ if (input.downlinkMbps !== void 0 && Number.isFinite(input.downlinkMbps) && input.downlinkMbps >= 0) {
33856
+ tier = capTo(input.downlinkMbps >= 8 ? "high" : input.downlinkMbps >= 2.5 ? "mid" : "low", available) ?? fallback;
33857
+ reason = "downlink";
33858
+ }
33859
+ const width = input.viewportWidth;
33860
+ const height = input.viewportHeight;
33861
+ if (width !== void 0 && height !== void 0 && width > 0 && height > 0) {
33862
+ if (Math.max(width, height) < 240) {
33863
+ const tiled = capTo("low", available) ?? fallback;
33864
+ if (RANK[tiled] < RANK[tier]) return {
33865
+ tier: tiled,
33866
+ reason: "viewport-ceiling"
33867
+ };
33868
+ }
33869
+ }
33870
+ return {
33871
+ tier,
33872
+ reason
33873
+ };
33874
+ }
33875
+ //#endregion
33705
33876
  //#region src/stream-broker/webrtc/broker-webrtc-server.ts
33706
33877
  /**
33707
33878
  * Broker-integrated WebRTC server.
@@ -33817,36 +33988,12 @@ function deriveH264ProfileLevelId(sdpParameterSets, preBuffer) {
33817
33988
  if (profileLevelId) return profileLevelId;
33818
33989
  }
33819
33990
  }
33820
- /** Fallback resolution/bitrate per label when the broker hasn't reported stats yet. */
33821
- var LABEL_DEFAULTS = {
33822
- high: {
33823
- pixels: 1920 * 1080,
33824
- bitrateKbps: 4e3
33825
- },
33826
- mid: {
33827
- pixels: 1280 * 720,
33828
- bitrateKbps: 1200
33829
- },
33830
- low: {
33831
- pixels: 640 * 360,
33832
- bitrateKbps: 400
33833
- }
33834
- };
33835
- var TIER_PREFERENCE = [
33836
- "high",
33837
- "mid",
33838
- "low"
33839
- ];
33840
- /** Smallest-keyframe-first tier order. Used as the fallback when an explicit
33841
- * `prefersTier` (e.g. the hub's `remote`-viewer bias to `low`) has no exact
33842
- * assigned slot: prefer the SMALLEST available tier rather than the
33843
- * quality-first default, so a remote-biased request never silently lands back
33844
- * on `high`. */
33845
- var SMALLEST_FIRST_TIER_PREFERENCE = [
33846
- "low",
33847
- "mid",
33848
- "high"
33849
- ];
33991
+ function adaptivePickLogLine(reason) {
33992
+ if (reason === "prefers-tier") return "Adaptive source selected (prefersTier exact)";
33993
+ if (reason === "prefers-tier-fallback") return "Adaptive source selected (prefersTier fallback → smallest available)";
33994
+ if (reason === "default-high") return "Adaptive source selected (no client hints — tier preference)";
33995
+ return "Adaptive source selected";
33996
+ }
33850
33997
  /** Push-queue overflow purges are aggregated and reported at most this often. */
33851
33998
  var FRAME_DROP_LOG_INTERVAL_MS = 3e4;
33852
33999
  /**
@@ -33887,20 +34034,6 @@ function reduceProfileForTranscodeDown(base) {
33887
34034
  }
33888
34035
  };
33889
34036
  }
33890
- function scoreBroker(tier, broker, hints) {
33891
- const stats = broker.getStats();
33892
- const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
33893
- const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
33894
- const srcPixels = fallback?.pixels ?? 1920 * 1080;
33895
- const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
33896
- const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
33897
- let bandwidthPenalty = 0;
33898
- if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
33899
- const availableKbps = hints.downlinkMbps * 1e3 * .6;
33900
- if (bitrateKbps > availableKbps) bandwidthPenalty = (bitrateKbps - availableKbps) / availableKbps * 100;
33901
- }
33902
- return pixelDistance + bandwidthPenalty;
33903
- }
33904
34037
  var BrokerWebrtcServer = class {
33905
34038
  logger;
33906
34039
  getIceServersFn;
@@ -35032,78 +35165,26 @@ var BrokerWebrtcServer = class {
35032
35165
  if (pool.length === 1) return pool[0].brokerId;
35033
35166
  const adaptiveDeviceIdTag = Number(deviceKey.split("/")[0]);
35034
35167
  const deviceIdTag = Number.isFinite(adaptiveDeviceIdTag) ? { tags: { deviceId: adaptiveDeviceIdTag } } : {};
35035
- if (hints.prefersTier === "high" || hints.prefersTier === "mid" || hints.prefersTier === "low") {
35036
- const explicit = pool.find((c) => c.tier === hints.prefersTier);
35037
- if (explicit) {
35038
- this.logger.info("Adaptive source selected (prefersTier exact)", {
35039
- ...deviceIdTag,
35040
- meta: {
35041
- deviceKey,
35042
- selected: explicit.brokerId,
35043
- tier: explicit.tier,
35044
- prefersTier: hints.prefersTier
35045
- }
35046
- });
35047
- return explicit.brokerId;
35048
- }
35049
- for (const tier of SMALLEST_FIRST_TIER_PREFERENCE) {
35050
- const match = pool.find((c) => c.tier === tier);
35051
- if (match) {
35052
- this.logger.info("Adaptive source selected (prefersTier fallback → smallest available)", {
35053
- ...deviceIdTag,
35054
- meta: {
35055
- deviceKey,
35056
- selected: match.brokerId,
35057
- tier: match.tier,
35058
- prefersTier: hints.prefersTier
35059
- }
35060
- });
35061
- return match.brokerId;
35062
- }
35063
- }
35064
- }
35065
- if (!(hints.viewportWidth !== void 0 || hints.viewportHeight !== void 0 || hints.downlinkMbps !== void 0)) {
35066
- for (const tier of TIER_PREFERENCE) {
35067
- const match = pool.find((c) => c.tier === tier);
35068
- if (match) {
35069
- this.logger.info("Adaptive source selected (no client hints — tier preference)", {
35070
- ...deviceIdTag,
35071
- meta: {
35072
- deviceKey,
35073
- selected: match.brokerId,
35074
- tier: match.tier
35075
- }
35076
- });
35077
- return match.brokerId;
35078
- }
35079
- }
35080
- return pool[0].brokerId;
35081
- }
35082
- let bestId = pool[0].brokerId;
35083
- let bestTier = pool[0].tier;
35084
- let bestScore = Infinity;
35085
- for (const c of pool) {
35086
- const score = scoreBroker(c.tier, c.broker, hints);
35087
- const cRank = c.tier ? TIER_PREFERENCE.indexOf(c.tier) : TIER_PREFERENCE.length;
35088
- const bestRank = bestTier ? TIER_PREFERENCE.indexOf(bestTier) : TIER_PREFERENCE.length;
35089
- if (score < bestScore || score === bestScore && cRank < bestRank) {
35090
- bestId = c.brokerId;
35091
- bestTier = c.tier;
35092
- bestScore = score;
35093
- }
35094
- }
35095
- const adaptiveDeviceId = Number(deviceKey.split("/")[0]);
35096
- this.logger.debug("Adaptive source selected", {
35097
- ...Number.isFinite(adaptiveDeviceId) ? { tags: { deviceId: adaptiveDeviceId } } : {},
35168
+ const pick = pickAdaptiveTier({
35169
+ available: pool.map((c) => c.tier),
35170
+ prefersTier: hints.prefersTier,
35171
+ downlinkMbps: hints.downlinkMbps,
35172
+ viewportWidth: hints.viewportWidth,
35173
+ viewportHeight: hints.viewportHeight
35174
+ });
35175
+ const selected = pool.find((c) => c.tier === pick.tier) ?? pool[0];
35176
+ this.logger.info(adaptivePickLogLine(pick.reason), {
35177
+ ...deviceIdTag,
35098
35178
  meta: {
35099
35179
  deviceKey,
35100
- selected: bestId,
35101
- tier: bestTier,
35102
- score: Math.round(bestScore * 100) / 100,
35180
+ selected: selected.brokerId,
35181
+ tier: selected.tier,
35182
+ reason: pick.reason,
35183
+ prefersTier: hints.prefersTier,
35103
35184
  hints
35104
35185
  }
35105
35186
  });
35106
- return bestId;
35187
+ return selected.brokerId;
35107
35188
  }
35108
35189
  async resolveIceServers() {
35109
35190
  if (this.getIceServersFn) try {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-w2XLD3uI.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_SFFNGT.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }