@camstack/addon-pipeline 1.2.129 → 1.2.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{addon-utils-CKB-2oM9.js → addon-utils-DCQ5AP_v.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +332 -20
- package/dist/detection-pipeline/index.mjs +330 -18
- package/dist/{dist-D9x2_csL.js → dist-CqoMNE1e.js} +2010 -1753
- package/dist/{dist-CMio-1qq.mjs → dist-DO0UVhOk.mjs} +2010 -1753
- package/dist/{event-loop-stall-monitor-B9dxYxyf.mjs → event-loop-stall-monitor-D0Juf7Dn.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-UW8nsiMb.js → event-loop-stall-monitor-eM7_yYfn.js} +1 -1
- package/dist/{lazy-sharp-DrLwLKpg.js → lazy-sharp-BGrpS8yi.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-C6es8dMA.mjs → process-memory-Bac3FuG6.mjs} +1 -1
- package/dist/{process-memory-CSJ7-Wlp.js → process-memory-xAPjvSw9.js} +1 -1
- package/dist/recorder/index.js +560 -31
- package/dist/recorder/index.mjs +558 -30
- package/dist/{segment-demux-js-DiLDoV9_.js → segment-demux-js-Cu0oDMUL.js} +1 -1
- package/dist/{segment-demux-js-BRCjXbI0.mjs → segment-demux-js-skhsAZx6.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-1O99xPTr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DGQkDEYw.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Do2DEtRt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CQKwHIHI.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-8scvCWua.mjs → hostInit-D7FwwZgj.mjs} +3 -3
- package/dist/stream-broker/index.js +426 -319
- package/dist/stream-broker/index.mjs +426 -319
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BmBYCioC.js → worker-protocol-G3HXTY8z.js} +1 -1
- package/dist/{worker-protocol-BHNWB9Ve.mjs → worker-protocol-_Ujvj1hl.mjs} +1 -1
- package/package.json +1 -1
- package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
- package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxppPmXr.mjs +0 -26
|
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-CqoMNE1e.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");
|
|
9
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
9
|
+
const require_segment_demux_js = require("../segment-demux-js-Cu0oDMUL.js");
|
|
10
10
|
let node_crypto = require("node:crypto");
|
|
11
11
|
node_crypto = require_dist.__toESM(node_crypto, 1);
|
|
12
12
|
let node_child_process = require("node:child_process");
|
|
@@ -23479,9 +23479,13 @@ var ControlMessageSchema = require_dist.discriminatedUnion("t", [
|
|
|
23479
23479
|
}),
|
|
23480
23480
|
require_dist.object({
|
|
23481
23481
|
t: require_dist.literal("scrubCommit"),
|
|
23482
|
-
epoch: require_dist.number()
|
|
23482
|
+
epoch: require_dist.number(),
|
|
23483
|
+
dragId: require_dist.string().optional()
|
|
23484
|
+
}),
|
|
23485
|
+
require_dist.object({
|
|
23486
|
+
t: require_dist.literal("scrubStart"),
|
|
23487
|
+
dragId: require_dist.string().optional()
|
|
23483
23488
|
}),
|
|
23484
|
-
require_dist.object({ t: require_dist.literal("scrubStart") }),
|
|
23485
23489
|
require_dist.object({
|
|
23486
23490
|
t: require_dist.literal("scrubTo"),
|
|
23487
23491
|
epoch: require_dist.number(),
|
|
@@ -23633,104 +23637,6 @@ var MonotonicClock = class {
|
|
|
23633
23637
|
this.rate = rate;
|
|
23634
23638
|
}
|
|
23635
23639
|
};
|
|
23636
|
-
var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 48 * 1024 * 1024;
|
|
23637
|
-
var SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES = 2 * 1024 * 1024;
|
|
23638
|
-
/** Demuxed bytes an entry holds — what the byte bound actually counts. */
|
|
23639
|
-
function fragmentBytes(frag) {
|
|
23640
|
-
let total = 0;
|
|
23641
|
-
for (const au of frag.aus) total += au.data.length;
|
|
23642
|
-
return total;
|
|
23643
|
-
}
|
|
23644
|
-
var ScrubFragmentCache = class {
|
|
23645
|
-
maxBytes;
|
|
23646
|
-
otherMaxBytes;
|
|
23647
|
-
map = /* @__PURE__ */ new Map();
|
|
23648
|
-
/**
|
|
23649
|
-
* Bytes held PER PROFILE. Budgets are per profile and so is eviction: a
|
|
23650
|
-
* `high` entry must never be able to evict the `low` segments the drag is
|
|
23651
|
-
* running on, and the reverse would be just as wrong.
|
|
23652
|
-
*/
|
|
23653
|
-
bytesByProfile = /* @__PURE__ */ new Map();
|
|
23654
|
-
constructor(maxBytes = SCRUB_FRAGMENT_CACHE_MAX_BYTES, otherMaxBytes = SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES) {
|
|
23655
|
-
this.maxBytes = maxBytes;
|
|
23656
|
-
this.otherMaxBytes = otherMaxBytes;
|
|
23657
|
-
}
|
|
23658
|
-
/** The byte budget for a profile — the scrub profile gets the real one. */
|
|
23659
|
-
budgetFor(profile) {
|
|
23660
|
-
return profile === "low" ? this.maxBytes : this.otherMaxBytes;
|
|
23661
|
-
}
|
|
23662
|
-
key(profile, startMs) {
|
|
23663
|
-
return `${profile}|${startMs}`;
|
|
23664
|
-
}
|
|
23665
|
-
/** Exact-start lookup with LRU touch (re-insertion marks most recent). */
|
|
23666
|
-
get(profile, startMs) {
|
|
23667
|
-
const k = this.key(profile, startMs);
|
|
23668
|
-
const frag = this.map.get(k);
|
|
23669
|
-
if (!frag) return null;
|
|
23670
|
-
this.map.delete(k);
|
|
23671
|
-
this.map.set(k, frag);
|
|
23672
|
-
return frag;
|
|
23673
|
-
}
|
|
23674
|
-
/**
|
|
23675
|
-
* The cached fragment COVERING an instant, if any — the lookup that lets a
|
|
23676
|
-
* warm drag tick skip `locate` entirely (an RPC measured at 1–8 s on a
|
|
23677
|
-
* saturated hub). Coverage is proof of footage: recorded fragments are
|
|
23678
|
-
* immutable, so a hit needs no re-validation. LRU-touches the hit.
|
|
23679
|
-
*/
|
|
23680
|
-
covering(profile, epochMs) {
|
|
23681
|
-
for (const [k, frag] of this.map) {
|
|
23682
|
-
if (!k.startsWith(`${profile}|`)) continue;
|
|
23683
|
-
if (epochMs >= frag.startMs && epochMs < frag.startMs + frag.durMs) {
|
|
23684
|
-
this.map.delete(k);
|
|
23685
|
-
this.map.set(k, frag);
|
|
23686
|
-
return frag;
|
|
23687
|
-
}
|
|
23688
|
-
}
|
|
23689
|
-
return null;
|
|
23690
|
-
}
|
|
23691
|
-
/** Insert (or refresh) a fragment, evicting that PROFILE's oldest past its bound. */
|
|
23692
|
-
put(profile, frag) {
|
|
23693
|
-
const k = this.key(profile, frag.startMs);
|
|
23694
|
-
const held = this.map.get(k);
|
|
23695
|
-
let bytes = this.bytesByProfile.get(profile) ?? 0;
|
|
23696
|
-
if (held) bytes -= fragmentBytes(held);
|
|
23697
|
-
this.map.delete(k);
|
|
23698
|
-
this.map.set(k, frag);
|
|
23699
|
-
bytes += fragmentBytes(frag);
|
|
23700
|
-
this.bytesByProfile.set(profile, bytes);
|
|
23701
|
-
const budget = this.budgetFor(profile);
|
|
23702
|
-
const prefix = `${profile}|`;
|
|
23703
|
-
while ((this.bytesByProfile.get(profile) ?? 0) > budget && this.entriesFor(profile) > 4) {
|
|
23704
|
-
const oldest = this.oldestKeyWithPrefix(prefix);
|
|
23705
|
-
if (oldest === void 0) break;
|
|
23706
|
-
const evicted = this.map.get(oldest);
|
|
23707
|
-
this.map.delete(oldest);
|
|
23708
|
-
if (evicted) this.bytesByProfile.set(profile, (this.bytesByProfile.get(profile) ?? 0) - fragmentBytes(evicted));
|
|
23709
|
-
}
|
|
23710
|
-
}
|
|
23711
|
-
oldestKeyWithPrefix(prefix) {
|
|
23712
|
-
for (const k of this.map.keys()) if (k.startsWith(prefix)) return k;
|
|
23713
|
-
}
|
|
23714
|
-
entriesFor(profile) {
|
|
23715
|
-
const prefix = `${profile}|`;
|
|
23716
|
-
let n = 0;
|
|
23717
|
-
for (const k of this.map.keys()) if (k.startsWith(prefix)) n += 1;
|
|
23718
|
-
return n;
|
|
23719
|
-
}
|
|
23720
|
-
/** Demuxed bytes currently held across every profile. */
|
|
23721
|
-
get byteSize() {
|
|
23722
|
-
let total = 0;
|
|
23723
|
-
for (const bytes of this.bytesByProfile.values()) total += bytes;
|
|
23724
|
-
return total;
|
|
23725
|
-
}
|
|
23726
|
-
/** Demuxed bytes held for one profile — the number its MB bound governs. */
|
|
23727
|
-
byteSizeFor(profile) {
|
|
23728
|
-
return this.bytesByProfile.get(profile) ?? 0;
|
|
23729
|
-
}
|
|
23730
|
-
get size() {
|
|
23731
|
-
return this.map.size;
|
|
23732
|
-
}
|
|
23733
|
-
};
|
|
23734
23640
|
/**
|
|
23735
23641
|
* A bounded numeric series. Exact count/sum/min/max forever; the median comes
|
|
23736
23642
|
* from the first {@link SCRUB_PHASE_SAMPLE_CAP} samples.
|
|
@@ -23796,6 +23702,9 @@ var ScrubDragStats = class {
|
|
|
23796
23702
|
prefetchBytes = 0;
|
|
23797
23703
|
prefetchHits = 0;
|
|
23798
23704
|
prefetchSuppressed = 0;
|
|
23705
|
+
warmReads = 0;
|
|
23706
|
+
warmBytes = 0;
|
|
23707
|
+
warmHits = 0;
|
|
23799
23708
|
ackTimeouts = 0;
|
|
23800
23709
|
deadlineMisses = 0;
|
|
23801
23710
|
retargets = 0;
|
|
@@ -23862,6 +23771,15 @@ var ScrubDragStats = class {
|
|
|
23862
23771
|
recordPrefetchHit() {
|
|
23863
23772
|
this.prefetchHits += 1;
|
|
23864
23773
|
}
|
|
23774
|
+
/** One window-warm read taken while the drag was live, and what it cost. */
|
|
23775
|
+
recordWarmRead(bytes) {
|
|
23776
|
+
this.warmReads += 1;
|
|
23777
|
+
this.warmBytes += bytes;
|
|
23778
|
+
}
|
|
23779
|
+
/** A tick served by a fragment the window warm had already landed. */
|
|
23780
|
+
recordWarmHit() {
|
|
23781
|
+
this.warmHits += 1;
|
|
23782
|
+
}
|
|
23865
23783
|
/** Pushes written off after the ack timeout — dropped work, never silent. */
|
|
23866
23784
|
recordAckTimeout(pushes) {
|
|
23867
23785
|
this.ackTimeouts += pushes;
|
|
@@ -23944,6 +23862,9 @@ var ScrubDragStats = class {
|
|
|
23944
23862
|
prefetchBytes: this.prefetchBytes,
|
|
23945
23863
|
prefetchHits: this.prefetchHits,
|
|
23946
23864
|
prefetchSuppressed: this.prefetchSuppressed,
|
|
23865
|
+
warmReads: this.warmReads,
|
|
23866
|
+
warmBytes: this.warmBytes,
|
|
23867
|
+
warmHits: this.warmHits,
|
|
23947
23868
|
ackTimeouts: this.ackTimeouts,
|
|
23948
23869
|
deadlineMisses: this.deadlineMisses,
|
|
23949
23870
|
retargets: this.retargets,
|
|
@@ -23963,8 +23884,158 @@ var ScrubDragStats = class {
|
|
|
23963
23884
|
};
|
|
23964
23885
|
}
|
|
23965
23886
|
};
|
|
23887
|
+
var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 48 * 1024 * 1024;
|
|
23888
|
+
var SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES = 2 * 1024 * 1024;
|
|
23889
|
+
/** Demuxed bytes an entry holds — what the byte bound actually counts. */
|
|
23890
|
+
function fragmentBytes(frag) {
|
|
23891
|
+
let total = 0;
|
|
23892
|
+
for (const au of frag.aus) total += au.data.length;
|
|
23893
|
+
return total;
|
|
23894
|
+
}
|
|
23895
|
+
var ScrubFragmentCache = class {
|
|
23896
|
+
maxBytes;
|
|
23897
|
+
otherMaxBytes;
|
|
23898
|
+
map = /* @__PURE__ */ new Map();
|
|
23899
|
+
/**
|
|
23900
|
+
* Bytes held PER PROFILE. Budgets are per profile and so is eviction: a
|
|
23901
|
+
* `high` entry must never be able to evict the `low` segments the drag is
|
|
23902
|
+
* running on, and the reverse would be just as wrong.
|
|
23903
|
+
*/
|
|
23904
|
+
bytesByProfile = /* @__PURE__ */ new Map();
|
|
23905
|
+
constructor(maxBytes = SCRUB_FRAGMENT_CACHE_MAX_BYTES, otherMaxBytes = SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES) {
|
|
23906
|
+
this.maxBytes = maxBytes;
|
|
23907
|
+
this.otherMaxBytes = otherMaxBytes;
|
|
23908
|
+
}
|
|
23909
|
+
/** The byte budget for a profile — the scrub profile gets the real one. */
|
|
23910
|
+
budgetFor(profile) {
|
|
23911
|
+
return profile === "low" ? this.maxBytes : this.otherMaxBytes;
|
|
23912
|
+
}
|
|
23913
|
+
key(profile, startMs) {
|
|
23914
|
+
return `${profile}|${startMs}`;
|
|
23915
|
+
}
|
|
23916
|
+
/** Exact-start lookup with LRU touch (re-insertion marks most recent). */
|
|
23917
|
+
get(profile, startMs) {
|
|
23918
|
+
const k = this.key(profile, startMs);
|
|
23919
|
+
const frag = this.map.get(k);
|
|
23920
|
+
if (!frag) return null;
|
|
23921
|
+
this.map.delete(k);
|
|
23922
|
+
this.map.set(k, frag);
|
|
23923
|
+
return frag;
|
|
23924
|
+
}
|
|
23925
|
+
/**
|
|
23926
|
+
* The cached fragment COVERING an instant, if any — the lookup that lets a
|
|
23927
|
+
* warm drag tick skip `locate` entirely (an RPC measured at 1–8 s on a
|
|
23928
|
+
* saturated hub). Coverage is proof of footage: recorded fragments are
|
|
23929
|
+
* immutable, so a hit needs no re-validation. LRU-touches the hit.
|
|
23930
|
+
*/
|
|
23931
|
+
covering(profile, epochMs) {
|
|
23932
|
+
for (const [k, frag] of this.map) {
|
|
23933
|
+
if (!k.startsWith(`${profile}|`)) continue;
|
|
23934
|
+
if (epochMs >= frag.startMs && epochMs < frag.startMs + frag.durMs) {
|
|
23935
|
+
this.map.delete(k);
|
|
23936
|
+
this.map.set(k, frag);
|
|
23937
|
+
return frag;
|
|
23938
|
+
}
|
|
23939
|
+
}
|
|
23940
|
+
return null;
|
|
23941
|
+
}
|
|
23942
|
+
/** Insert (or refresh) a fragment, evicting that PROFILE's oldest past its bound. */
|
|
23943
|
+
put(profile, frag) {
|
|
23944
|
+
const k = this.key(profile, frag.startMs);
|
|
23945
|
+
const held = this.map.get(k);
|
|
23946
|
+
let bytes = this.bytesByProfile.get(profile) ?? 0;
|
|
23947
|
+
if (held) bytes -= fragmentBytes(held);
|
|
23948
|
+
this.map.delete(k);
|
|
23949
|
+
this.map.set(k, frag);
|
|
23950
|
+
bytes += fragmentBytes(frag);
|
|
23951
|
+
this.bytesByProfile.set(profile, bytes);
|
|
23952
|
+
const budget = this.budgetFor(profile);
|
|
23953
|
+
const prefix = `${profile}|`;
|
|
23954
|
+
while ((this.bytesByProfile.get(profile) ?? 0) > budget && this.entriesFor(profile) > 4) {
|
|
23955
|
+
const oldest = this.oldestKeyWithPrefix(prefix);
|
|
23956
|
+
if (oldest === void 0) break;
|
|
23957
|
+
const evicted = this.map.get(oldest);
|
|
23958
|
+
this.map.delete(oldest);
|
|
23959
|
+
if (evicted) this.bytesByProfile.set(profile, (this.bytesByProfile.get(profile) ?? 0) - fragmentBytes(evicted));
|
|
23960
|
+
}
|
|
23961
|
+
}
|
|
23962
|
+
oldestKeyWithPrefix(prefix) {
|
|
23963
|
+
for (const k of this.map.keys()) if (k.startsWith(prefix)) return k;
|
|
23964
|
+
}
|
|
23965
|
+
entriesFor(profile) {
|
|
23966
|
+
const prefix = `${profile}|`;
|
|
23967
|
+
let n = 0;
|
|
23968
|
+
for (const k of this.map.keys()) if (k.startsWith(prefix)) n += 1;
|
|
23969
|
+
return n;
|
|
23970
|
+
}
|
|
23971
|
+
/** Demuxed bytes currently held across every profile. */
|
|
23972
|
+
get byteSize() {
|
|
23973
|
+
let total = 0;
|
|
23974
|
+
for (const bytes of this.bytesByProfile.values()) total += bytes;
|
|
23975
|
+
return total;
|
|
23976
|
+
}
|
|
23977
|
+
/** Demuxed bytes held for one profile — the number its MB bound governs. */
|
|
23978
|
+
byteSizeFor(profile) {
|
|
23979
|
+
return this.bytesByProfile.get(profile) ?? 0;
|
|
23980
|
+
}
|
|
23981
|
+
get size() {
|
|
23982
|
+
return this.map.size;
|
|
23983
|
+
}
|
|
23984
|
+
};
|
|
23985
|
+
/**
|
|
23986
|
+
* `velocity` is ms of timeline per ms of wall clock, signed. `readMs` is what a
|
|
23987
|
+
* read costs on this disk right now. Either being null means "not enough is
|
|
23988
|
+
* known to aim" — which is NOT saturation: an unknown velocity must not switch
|
|
23989
|
+
* speculation off, or the first ticks of every drag would lose it.
|
|
23990
|
+
*/
|
|
23991
|
+
function scrubLeadDecision(velocity, readMs) {
|
|
23992
|
+
if (velocity === null || readMs === null) return {
|
|
23993
|
+
leadMs: 0,
|
|
23994
|
+
saturated: false
|
|
23995
|
+
};
|
|
23996
|
+
const raw = Math.round(velocity * readMs);
|
|
23997
|
+
if (Math.abs(raw) > 3e4) return {
|
|
23998
|
+
leadMs: 0,
|
|
23999
|
+
saturated: true
|
|
24000
|
+
};
|
|
24001
|
+
return {
|
|
24002
|
+
leadMs: raw,
|
|
24003
|
+
saturated: false
|
|
24004
|
+
};
|
|
24005
|
+
}
|
|
23966
24006
|
//#endregion
|
|
23967
24007
|
//#region src/stream-broker/recorded/recorded-feeder.ts
|
|
24008
|
+
/**
|
|
24009
|
+
* READ-AHEAD LEAD — the fix for "the picture chases the finger".
|
|
24010
|
+
*
|
|
24011
|
+
* A scrub frame is always the frame a read STARTED for, one read time ago.
|
|
24012
|
+
* On this disk that is 531 ms of wall clock (p50, census 2026-08-25), and on
|
|
24013
|
+
* a wide window 531 ms of gesture is TWENTY MINUTES of timeline: measured in
|
|
24014
|
+
* the drag bench, the median painted frame on a 2 h fling was 1 220 000 ms
|
|
24015
|
+
* behind the finger. Nothing about that is I/O — it is aiming at where the
|
|
24016
|
+
* finger WAS.
|
|
24017
|
+
*
|
|
24018
|
+
* So a steady drag reads where the finger WILL BE: `mediaVelocity ×
|
|
24019
|
+
* medianRecentReadMs`. It costs nothing (the same one read, at a different
|
|
24020
|
+
* offset), and it is bounded by the two things that make it a prediction
|
|
24021
|
+
* rather than a guess:
|
|
24022
|
+
*
|
|
24023
|
+
* - a direction RUN of at least {@link SCRUB_LEAD_MIN_DIR_RUN} POINTER
|
|
24024
|
+
* samples, so a jittery or reversing drag never leads. Pointer samples and
|
|
24025
|
+
* not pushes: on this disk a 3 s drag produces ~75 pointer samples and only
|
|
24026
|
+
* ~4 pushes, so a gate counted in pushes has no runway inside the gesture
|
|
24027
|
+
* it is supposed to help — measured in the drag bench, it engaged once, at
|
|
24028
|
+
* the very end;
|
|
24029
|
+
* - at least {@link SCRUB_LEAD_MIN_READ_SAMPLES} measured read, so the lead
|
|
24030
|
+
* is derived from this disk right now and not from a constant. ONE is
|
|
24031
|
+
* enough: against a read cost that is unknown, a one-sample estimate is not
|
|
24032
|
+
* a good prediction — it is simply a far better one than assuming zero.
|
|
24033
|
+
*
|
|
24034
|
+
* Overshoot is self-correcting: the retarget below serves the newest target
|
|
24035
|
+
* the landed fragment covers, so a lead that is slightly long lands on the
|
|
24036
|
+
* finger exactly, and one that is wrong costs the same single read it would
|
|
24037
|
+
* have cost anyway.
|
|
24038
|
+
*/
|
|
23968
24039
|
var SEGMENT_TIMEOUT_MS = 8e3;
|
|
23969
24040
|
/**
|
|
23970
24041
|
* Deadline for the recorded-AUDIO demux on the `reusedWhole` seek path, and
|
|
@@ -24065,30 +24136,6 @@ var SEEK_ANIMATION_MIN_DISTANCE_MS = 8e3;
|
|
|
24065
24136
|
*/
|
|
24066
24137
|
var SCRUB_JUMP_TRAVEL_MAX_STILLS = 3;
|
|
24067
24138
|
/**
|
|
24068
|
-
* Consecutive same-direction `scrubTo` deltas required before a direction-aware
|
|
24069
|
-
* prefetch commits its (single) background slot. Two deltas ⇒ the drag shows a
|
|
24070
|
-
* sustained direction, so a jittery/reversing drag never wastes the slot.
|
|
24071
|
-
*/
|
|
24072
|
-
var SCRUB_PREFETCH_DIR_THRESHOLD = 2;
|
|
24073
|
-
/**
|
|
24074
|
-
* How many speculative GOP reads may be in flight at once, i.e. how many drag
|
|
24075
|
-
* steps ahead the feeder reads.
|
|
24076
|
-
*
|
|
24077
|
-
* The scrub tick IS the read: measured on salone 590 over `low`,
|
|
24078
|
-
* `tickMsP50 = 270` of which `readMsP50 = 263` — a cold ~135 KB fragment off
|
|
24079
|
-
* the HDD array through shfs/FUSE, `demuxMsP50 = 1`. One read at a time is a
|
|
24080
|
-
* hard ~3.8 push/s ceiling no cache can lift, because a drag whose steps are
|
|
24081
|
-
* tens of seconds apart revisits nothing. Reading N steps ahead is the only
|
|
24082
|
-
* thing that converts wall-clock the drag is ALREADY spending into pushes.
|
|
24083
|
-
*
|
|
24084
|
-
* 2 and not more: these reads land on the same spindles the recorder is writing
|
|
24085
|
-
* every camera to, and past a small number concurrent random reads stop
|
|
24086
|
-
* overlapping and start thrashing. Raise it only against a measured
|
|
24087
|
-
* `prefetchHits` / `readMsP50` pair — it is the knob, and the census carries
|
|
24088
|
-
* both halves of the evidence.
|
|
24089
|
-
*/
|
|
24090
|
-
var SCRUB_PREFETCH_MAX_INFLIGHT = 2;
|
|
24091
|
-
/**
|
|
24092
24139
|
* The profile whose scrub read unit is the WHOLE SEGMENT.
|
|
24093
24140
|
*
|
|
24094
24141
|
* Every drag is pinned to `low` by the client, and a `low` segment is ~230 KB
|
|
@@ -24124,14 +24171,30 @@ var SCRUB_WHOLE_SEGMENT_PROFILE = "low";
|
|
|
24124
24171
|
* (`scrub-feeder:oversize-segment`, `oversizeSegments` on the drag census)
|
|
24125
24172
|
* instead of being inferred from a falling hit rate.
|
|
24126
24173
|
*
|
|
24127
|
-
* The real fix, when 60 s segments land, is a read unit that is a BYTE
|
|
24128
|
-
* over the fragment's own keyframe table — "as many consecutive GOPs
|
|
24129
|
-
* N bytes" —
|
|
24130
|
-
* (`
|
|
24131
|
-
* on
|
|
24174
|
+
* The real fix, for when 60 s segments land, is a read unit that is a BYTE
|
|
24175
|
+
* BUDGET over the fragment's own keyframe table — "as many consecutive GOPs
|
|
24176
|
+
* as fit in N bytes" — computed from `tfra` offsets alone, no payload read
|
|
24177
|
+
* (`fragmentsWithinBudget`, `recorder/mfra-index.ts`). `readGopBytes` now uses
|
|
24178
|
+
* exactly this budget on the fallback path below: it no longer reads exactly
|
|
24179
|
+
* one GOP, it reads as many CONSECUTIVE fragments as fit this same ceiling, in
|
|
24180
|
+
* one range read (`gop-range-read.ts`'s `maxBytes`). The recorder-side handler
|
|
24181
|
+
* (`recording-provider.ts`) declares its OWN copy of this number
|
|
24182
|
+
* (`SCRUB_GOP_BUDGET_MAX_BYTES` in `gop-range-read.ts`) rather than importing
|
|
24183
|
+
* this module — the client feeder is a different addon-internal concern, and
|
|
24184
|
+
* pulling its whole dependency graph in for one number is the wrong
|
|
24185
|
+
* direction. Exported here so a future caller in THIS process can reuse it;
|
|
24186
|
+
* if D163's number moves, move both.
|
|
24132
24187
|
*/
|
|
24133
24188
|
var SCRUB_WHOLE_SEGMENT_MAX_BYTES = 15e5;
|
|
24134
24189
|
/**
|
|
24190
|
+
* How still the finger must be before the window warm may take the disk back.
|
|
24191
|
+
*
|
|
24192
|
+
* Long enough that it does not thrash on the gap between two ticks of a slow
|
|
24193
|
+
* drag (tickMsP50 was 120-129 ms on the measured drags), short enough that a
|
|
24194
|
+
* pause the operator would call "stopped" is warm again before they move.
|
|
24195
|
+
*/
|
|
24196
|
+
var SCRUB_WARM_QUIET_MS = 300;
|
|
24197
|
+
/**
|
|
24135
24198
|
* How far the entry warm-up reaches, backward and forward from the playhead the
|
|
24136
24199
|
* drag starts on.
|
|
24137
24200
|
*
|
|
@@ -24166,17 +24229,6 @@ var SCRUB_WARM_MAX_SEGMENTS = Math.max(1, Math.floor(SCRUB_FRAGMENT_CACHE_MAX_BY
|
|
|
24166
24229
|
*/
|
|
24167
24230
|
var IDLE_WARM_READ_GAP_MS = 250;
|
|
24168
24231
|
/**
|
|
24169
|
-
* Median recent read time at or above which the feeder STOPS speculating.
|
|
24170
|
-
*
|
|
24171
|
-
* Speculation is a bet that spare I/O exists. When reads are this slow there is
|
|
24172
|
-
* no spare I/O — the array is saturated — and a prefetch is not free work, it
|
|
24173
|
-
* is another random seek queued ahead of the read the operator is waiting on.
|
|
24174
|
-
* The field regime that forced this: 54 prefetch reads for 9 hits with the disk
|
|
24175
|
-
* at 98% busy. Suppression is counted (`prefetchSuppressed`) so the decision is
|
|
24176
|
-
* visible, and it lifts by itself as soon as reads come back.
|
|
24177
|
-
*/
|
|
24178
|
-
var SCRUB_SLOW_READ_MS = 600;
|
|
24179
|
-
/**
|
|
24180
24232
|
* Max forward TARGET delta (ms of timeline, between consecutive drag targets)
|
|
24181
24233
|
* that SWEEPS predicted frames instead of jumping. A steady forward drag is
|
|
24182
24234
|
* playback with a moving target: once a keyframe decoded, the predicted frames
|
|
@@ -24249,37 +24301,6 @@ var SCRUB_RIDE_WINDOW_MS = 1e4;
|
|
|
24249
24301
|
* moved on to. At the cap, a tick rides the newest in-flight read instead.
|
|
24250
24302
|
*/
|
|
24251
24303
|
var SCRUB_RESOLVE_MAX_INFLIGHT = 2;
|
|
24252
|
-
/**
|
|
24253
|
-
* READ-AHEAD LEAD — the fix for "the picture chases the finger".
|
|
24254
|
-
*
|
|
24255
|
-
* A scrub frame is always the frame a read STARTED for, one read time ago.
|
|
24256
|
-
* On this disk that is 531 ms of wall clock (p50, census 2026-08-25), and on
|
|
24257
|
-
* a wide window 531 ms of gesture is TWENTY MINUTES of timeline: measured in
|
|
24258
|
-
* the drag bench, the median painted frame on a 2 h fling was 1 220 000 ms
|
|
24259
|
-
* behind the finger. Nothing about that is I/O — it is aiming at where the
|
|
24260
|
-
* finger WAS.
|
|
24261
|
-
*
|
|
24262
|
-
* So a steady drag reads where the finger WILL BE: `mediaVelocity ×
|
|
24263
|
-
* medianRecentReadMs`. It costs nothing (the same one read, at a different
|
|
24264
|
-
* offset), and it is bounded by the two things that make it a prediction
|
|
24265
|
-
* rather than a guess:
|
|
24266
|
-
*
|
|
24267
|
-
* - a direction RUN of at least {@link SCRUB_LEAD_MIN_DIR_RUN} POINTER
|
|
24268
|
-
* samples, so a jittery or reversing drag never leads. Pointer samples and
|
|
24269
|
-
* not pushes: on this disk a 3 s drag produces ~75 pointer samples and only
|
|
24270
|
-
* ~4 pushes, so a gate counted in pushes has no runway inside the gesture
|
|
24271
|
-
* it is supposed to help — measured in the drag bench, it engaged once, at
|
|
24272
|
-
* the very end;
|
|
24273
|
-
* - at least {@link SCRUB_LEAD_MIN_READ_SAMPLES} measured read, so the lead
|
|
24274
|
-
* is derived from this disk right now and not from a constant. ONE is
|
|
24275
|
-
* enough: against a read cost that is unknown, a one-sample estimate is not
|
|
24276
|
-
* a good prediction — it is simply a far better one than assuming zero.
|
|
24277
|
-
*
|
|
24278
|
-
* Overshoot is self-correcting: the retarget below serves the newest target
|
|
24279
|
-
* the landed fragment covers, so a lead that is slightly long lands on the
|
|
24280
|
-
* finger exactly, and one that is wrong costs the same single read it would
|
|
24281
|
-
* have cost anyway.
|
|
24282
|
-
*/
|
|
24283
24304
|
var SCRUB_LEAD_MIN_READ_SAMPLES = 1;
|
|
24284
24305
|
/**
|
|
24285
24306
|
* Smoothing for the media-velocity estimate (ms of timeline per ms of wall
|
|
@@ -24512,25 +24533,6 @@ var RecordedFeeder = class {
|
|
|
24512
24533
|
scrubLastPushLogAtMs = 0;
|
|
24513
24534
|
scrubLastSlowLocateLogAtMs = 0;
|
|
24514
24535
|
/**
|
|
24515
|
-
* Signed run-length of consecutive same-direction `scrubTo` deltas (+ forward,
|
|
24516
|
-
* − backward). A prefetch commits only once `|run| ≥ SCRUB_PREFETCH_DIR_THRESHOLD`;
|
|
24517
|
-
* a reversal or an in-fragment no-move resets it.
|
|
24518
|
-
*/
|
|
24519
|
-
scrubDirRun = 0;
|
|
24520
|
-
/**
|
|
24521
|
-
* Prefetch reads in flight. Bounded by {@link SCRUB_PREFETCH_MAX_INFLIGHT}:
|
|
24522
|
-
* these are the SPECULATIVE concurrency slots, and they only fill
|
|
24523
|
-
* `scrubCache` — they never push, never touch the pending/timer/latest-wins
|
|
24524
|
-
* state, and never block the live coalesced tick.
|
|
24525
|
-
*
|
|
24526
|
-
* More than one because the read IS the tick (263 of 270 ms measured): with a
|
|
24527
|
-
* single slot the drag can never go faster than one cold read, however well
|
|
24528
|
-
* the speculation guesses.
|
|
24529
|
-
*/
|
|
24530
|
-
scrubPrefetchInFlight = 0;
|
|
24531
|
-
/** Probe instants currently being read, so two horizons never duplicate one. */
|
|
24532
|
-
scrubPrefetchProbes = /* @__PURE__ */ new Set();
|
|
24533
|
-
/**
|
|
24534
24536
|
* Fragment starts this drag put in the cache SPECULATIVELY. A later tick that
|
|
24535
24537
|
* hits one of them is what makes speculation provably worth its I/O
|
|
24536
24538
|
* (`prefetchHits`) — without this the feeder could double the load on the disk
|
|
@@ -24580,6 +24582,18 @@ var RecordedFeeder = class {
|
|
|
24580
24582
|
scrubFillLoopActive = false;
|
|
24581
24583
|
scrubWarmBackMs = SCRUB_WARM_BACK_MS;
|
|
24582
24584
|
scrubWarmFwdMs = SCRUB_WARM_FWD_MS;
|
|
24585
|
+
/**
|
|
24586
|
+
* The client-supplied id of the CURRENT (or most recently active) scrub
|
|
24587
|
+
* gesture, set by `beginScrub(dragId)`. Printed on every `scrub-feeder:*`
|
|
24588
|
+
* line — ticks, warm fill, park/deadline/ack pathologies, and the drag-end
|
|
24589
|
+
* aggregate — so a drag can be isolated in the logs without depending on
|
|
24590
|
+
* wall-clock proximity. `undefined` outside a drag (and for a feeder whose
|
|
24591
|
+
* caller never sends one — additive, optional, see D1 in the plan).
|
|
24592
|
+
*/
|
|
24593
|
+
scrubDragId;
|
|
24594
|
+
/** This feeder's own last-announced state — the `from` half of every
|
|
24595
|
+
* {@link FeederStateTransition}. `null` before the first transition. */
|
|
24596
|
+
lastAnnouncedState = null;
|
|
24583
24597
|
constructor(deps) {
|
|
24584
24598
|
this.deps = deps;
|
|
24585
24599
|
this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3);
|
|
@@ -24597,6 +24611,50 @@ var RecordedFeeder = class {
|
|
|
24597
24611
|
if (typeof pcmu === "function") return (data, rtpTs) => pcmu.call(sink, data, rtpTs);
|
|
24598
24612
|
}
|
|
24599
24613
|
/**
|
|
24614
|
+
* Emit one state transition through `onState` — see
|
|
24615
|
+
* {@link FeederStateTransition} for why `from`/`reason`/`seekToken` exist.
|
|
24616
|
+
* The ONLY place `this.lastAnnouncedState` is written, so every announced
|
|
24617
|
+
* state (wherever it is announced from) is captured as the next
|
|
24618
|
+
* transition's `from`.
|
|
24619
|
+
*/
|
|
24620
|
+
emitState(to, reason, seekToken, dragId) {
|
|
24621
|
+
const from = this.lastAnnouncedState;
|
|
24622
|
+
this.lastAnnouncedState = to;
|
|
24623
|
+
this.deps.onState({
|
|
24624
|
+
from,
|
|
24625
|
+
to,
|
|
24626
|
+
reason,
|
|
24627
|
+
seekToken,
|
|
24628
|
+
dragId
|
|
24629
|
+
});
|
|
24630
|
+
}
|
|
24631
|
+
/**
|
|
24632
|
+
* ONE line per `seek()` call, unconditionally — the C3 rendiconto. Before
|
|
24633
|
+
* this, a commit's outcome was told by WHICHEVER of several lines happened
|
|
24634
|
+
* to fire: `recorded:seek-ready` only on success, `recorded:seek-abandoned`
|
|
24635
|
+
* only when a later token/dispose superseded this one, `recorded:seek-no-footage`
|
|
24636
|
+
* / `recorded:snap-too-far` only on a gap — and a raw thrown error (or an
|
|
24637
|
+
* error on an ALREADY-abandoned commit) logged NOTHING at all. That silent
|
|
24638
|
+
* shape is exactly the 2026-08-26 stall: two commits reached
|
|
24639
|
+
* `playback:intent` and never `playback:ready`, with nothing in the feeder's
|
|
24640
|
+
* own logs saying why. Every return path in `seek()` now calls this before
|
|
24641
|
+
* it exits, so a commit that never lands still leaves one line naming what
|
|
24642
|
+
* happened to it.
|
|
24643
|
+
*/
|
|
24644
|
+
logSeekOutcome(token, seekStartedAt, epochMs, dragId, outcome, state) {
|
|
24645
|
+
this.deps.onLog?.("recorded:seek-outcome", {
|
|
24646
|
+
dragId,
|
|
24647
|
+
epochMs,
|
|
24648
|
+
outcome,
|
|
24649
|
+
state,
|
|
24650
|
+
disposed: this.disposed,
|
|
24651
|
+
superseded: token !== this.seekToken,
|
|
24652
|
+
tokenAtStart: token,
|
|
24653
|
+
tokenNow: this.seekToken,
|
|
24654
|
+
totalMs: Math.round(performance.now() - seekStartedAt)
|
|
24655
|
+
});
|
|
24656
|
+
}
|
|
24657
|
+
/**
|
|
24600
24658
|
* Seek to an epoch and (if rate > 0) begin forward playback. Resolves after
|
|
24601
24659
|
* the FIRST located segment is fed; the forward `advance` loop is started
|
|
24602
24660
|
* fire-and-forget so seek never blocks on its pacing. Latest-wins: a newer
|
|
@@ -24609,6 +24667,7 @@ var RecordedFeeder = class {
|
|
|
24609
24667
|
let loadMs = 0;
|
|
24610
24668
|
let firstFrameMs = 0;
|
|
24611
24669
|
const token = ++this.seekToken;
|
|
24670
|
+
const dragId = opts?.dragId;
|
|
24612
24671
|
const wasScrubbing = this.scrubbing;
|
|
24613
24672
|
if (this.scrubbing) {
|
|
24614
24673
|
this.emitDragEnd();
|
|
@@ -24620,7 +24679,6 @@ var RecordedFeeder = class {
|
|
|
24620
24679
|
this.clearScrubAckTimeout();
|
|
24621
24680
|
this.scrubPendingHint = null;
|
|
24622
24681
|
this.scrubPrevTarget = null;
|
|
24623
|
-
this.scrubDirRun = 0;
|
|
24624
24682
|
if (this.scrubTimer) {
|
|
24625
24683
|
clearTimeout(this.scrubTimer);
|
|
24626
24684
|
this.scrubTimer = null;
|
|
@@ -24630,7 +24688,7 @@ var RecordedFeeder = class {
|
|
|
24630
24688
|
this.inFlightSeekEpochMs = epochMs;
|
|
24631
24689
|
let snapInfo = null;
|
|
24632
24690
|
try {
|
|
24633
|
-
this.
|
|
24691
|
+
this.emitState("loading", "seek-start", token, dragId);
|
|
24634
24692
|
if (this.clockHasAdvanced) {
|
|
24635
24693
|
this.clock.markDiscontinuity(SEEK_DISCONTINUITY_MS);
|
|
24636
24694
|
this.audioClock.markDiscontinuity(SEEK_DISCONTINUITY_MS);
|
|
@@ -24642,21 +24700,29 @@ var RecordedFeeder = class {
|
|
|
24642
24700
|
epochMs
|
|
24643
24701
|
}));
|
|
24644
24702
|
locateMs += performance.now() - locateStartedAt;
|
|
24645
|
-
if (this.disposed || token !== this.seekToken)
|
|
24703
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24704
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24705
|
+
return;
|
|
24706
|
+
}
|
|
24646
24707
|
let landMs = epochMs;
|
|
24647
24708
|
if (loc.kind === "gap") {
|
|
24648
24709
|
const decision = resolveSnapTarget(loc, epochMs);
|
|
24649
24710
|
if (decision.kind !== "snap") {
|
|
24650
24711
|
if (decision.kind === "too-far") this.deps.onLog?.("recorded:snap-too-far", {
|
|
24712
|
+
dragId,
|
|
24651
24713
|
requestedMs: epochMs,
|
|
24652
24714
|
nearestMs: decision.snapMs,
|
|
24653
24715
|
distanceMs: decision.distanceMs
|
|
24654
24716
|
});
|
|
24655
|
-
else this.deps.onLog?.("recorded:seek-no-footage", {
|
|
24717
|
+
else this.deps.onLog?.("recorded:seek-no-footage", {
|
|
24718
|
+
dragId,
|
|
24719
|
+
requestedMs: epochMs
|
|
24720
|
+
});
|
|
24656
24721
|
this.currentSegment = null;
|
|
24657
24722
|
this.lastFedVideoIdx = -1;
|
|
24658
24723
|
this.stopPositionReports();
|
|
24659
|
-
this.
|
|
24724
|
+
this.emitState("gap", decision.kind === "too-far" ? "seek-snap-too-far" : "seek-no-footage", token, dragId);
|
|
24725
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "gap", "gap");
|
|
24660
24726
|
return;
|
|
24661
24727
|
}
|
|
24662
24728
|
const snapLocateStartedAt = performance.now();
|
|
@@ -24666,19 +24732,25 @@ var RecordedFeeder = class {
|
|
|
24666
24732
|
epochMs: decision.snapMs
|
|
24667
24733
|
}));
|
|
24668
24734
|
locateMs += performance.now() - snapLocateStartedAt;
|
|
24669
|
-
if (this.disposed || token !== this.seekToken)
|
|
24735
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24736
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24737
|
+
return;
|
|
24738
|
+
}
|
|
24670
24739
|
if (snapped.kind !== "segment") {
|
|
24671
24740
|
this.deps.onLog?.("recorded:seek-no-footage", {
|
|
24741
|
+
dragId,
|
|
24672
24742
|
requestedMs: epochMs,
|
|
24673
24743
|
snappedMs: decision.snapMs
|
|
24674
24744
|
});
|
|
24675
24745
|
this.currentSegment = null;
|
|
24676
24746
|
this.lastFedVideoIdx = -1;
|
|
24677
24747
|
this.stopPositionReports();
|
|
24678
|
-
this.
|
|
24748
|
+
this.emitState("gap", "seek-snap-target-no-footage", token, dragId);
|
|
24749
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "gap", "gap");
|
|
24679
24750
|
return;
|
|
24680
24751
|
}
|
|
24681
24752
|
this.deps.onLog?.("recorded:seek-snapped", {
|
|
24753
|
+
dragId,
|
|
24682
24754
|
requestedMs: epochMs,
|
|
24683
24755
|
snappedMs: decision.snapMs
|
|
24684
24756
|
});
|
|
@@ -24692,9 +24764,12 @@ var RecordedFeeder = class {
|
|
|
24692
24764
|
const travelFromMs = opts?.travelFromMs ?? (this.clockHasAdvanced ? this.cursorMs : null);
|
|
24693
24765
|
if (!wasScrubbing && travelFromMs !== null) {
|
|
24694
24766
|
const travelStartedAt = performance.now();
|
|
24695
|
-
await this.animateSeekTravel(travelFromMs, landMs, token);
|
|
24767
|
+
await this.animateSeekTravel(travelFromMs, landMs, token, dragId);
|
|
24696
24768
|
travelMs = performance.now() - travelStartedAt;
|
|
24697
|
-
if (this.disposed || token !== this.seekToken)
|
|
24769
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24770
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24771
|
+
return;
|
|
24772
|
+
}
|
|
24698
24773
|
}
|
|
24699
24774
|
this.cursorMs = loc.startMs;
|
|
24700
24775
|
const loadStartedAt = performance.now();
|
|
@@ -24711,7 +24786,10 @@ var RecordedFeeder = class {
|
|
|
24711
24786
|
prefixSource = "cache";
|
|
24712
24787
|
} else {
|
|
24713
24788
|
const fragmentLoad = await this.loadScrubFragment(loc, landMs);
|
|
24714
|
-
if (fragmentLoad === null)
|
|
24789
|
+
if (fragmentLoad === null) {
|
|
24790
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24791
|
+
return;
|
|
24792
|
+
}
|
|
24715
24793
|
fragment = fragmentLoad.seg;
|
|
24716
24794
|
this.scrubCache.put(this.deps.profile, fragment);
|
|
24717
24795
|
prefixSource = fragmentLoad.wholeSegmentRead ? "whole" : "gop";
|
|
@@ -24720,7 +24798,10 @@ var RecordedFeeder = class {
|
|
|
24720
24798
|
bytes: fragmentLoad.rawBytes
|
|
24721
24799
|
};
|
|
24722
24800
|
}
|
|
24723
|
-
if (this.disposed || token !== this.seekToken)
|
|
24801
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24802
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24803
|
+
return;
|
|
24804
|
+
}
|
|
24724
24805
|
const fragmentLoc = {
|
|
24725
24806
|
kind: "segment",
|
|
24726
24807
|
startMs: fragment.startMs,
|
|
@@ -24731,7 +24812,7 @@ var RecordedFeeder = class {
|
|
|
24731
24812
|
prefixFed = true;
|
|
24732
24813
|
gopFirst = true;
|
|
24733
24814
|
firstFrameMs = performance.now() - seekStartedAt;
|
|
24734
|
-
this.
|
|
24815
|
+
this.emitState(this.rate === 0 ? "paused" : "playing", "seek-gop-prefix-landed", token, dragId);
|
|
24735
24816
|
this.deps.onPosition(this.cursorMs);
|
|
24736
24817
|
this.startPositionReports();
|
|
24737
24818
|
stateAnnounced = true;
|
|
@@ -24746,6 +24827,7 @@ var RecordedFeeder = class {
|
|
|
24746
24827
|
audio = [...await this.withTimeout(this.deps.demuxAudio(reusedWhole.bytes), AUDIO_DEMUX_TIMEOUT_MS)];
|
|
24747
24828
|
} catch (err) {
|
|
24748
24829
|
this.deps.onLog?.("recorded:seek-audio-dropped", {
|
|
24830
|
+
dragId,
|
|
24749
24831
|
waitedMs: Date.now() - audioStartedAt,
|
|
24750
24832
|
budgetMs: AUDIO_DEMUX_TIMEOUT_MS,
|
|
24751
24833
|
bytes: reusedWhole.bytes.byteLength,
|
|
@@ -24773,7 +24855,21 @@ var RecordedFeeder = class {
|
|
|
24773
24855
|
loadAusMs = performance.now() - loadAusStartedAt;
|
|
24774
24856
|
}
|
|
24775
24857
|
loadMs = performance.now() - loadStartedAt;
|
|
24776
|
-
if (this.disposed || token !== this.seekToken || loaded === null)
|
|
24858
|
+
if (this.disposed || token !== this.seekToken || loaded === null) {
|
|
24859
|
+
this.deps.onLog?.("recorded:seek-abandoned", {
|
|
24860
|
+
dragId,
|
|
24861
|
+
requestedMs: epochMs,
|
|
24862
|
+
disposed: this.disposed,
|
|
24863
|
+
superseded: token !== this.seekToken,
|
|
24864
|
+
tokenAtStart: token,
|
|
24865
|
+
tokenNow: this.seekToken,
|
|
24866
|
+
loadedNull: loaded === null,
|
|
24867
|
+
announcedLoading: true,
|
|
24868
|
+
loadMs: Math.round(loadMs)
|
|
24869
|
+
});
|
|
24870
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24871
|
+
return;
|
|
24872
|
+
}
|
|
24777
24873
|
if (!prefixFed) this.feedPrefix(loc, loaded.video, landMs);
|
|
24778
24874
|
if (gopFirst) {
|
|
24779
24875
|
this.currentSegment = {
|
|
@@ -24795,11 +24891,12 @@ var RecordedFeeder = class {
|
|
|
24795
24891
|
}
|
|
24796
24892
|
if (this.rate === 0) {
|
|
24797
24893
|
if (!stateAnnounced) {
|
|
24798
|
-
this.
|
|
24894
|
+
this.emitState("paused", "seek-landed-paused", token, dragId);
|
|
24799
24895
|
this.deps.onPosition(this.cursorMs);
|
|
24800
24896
|
this.startPositionReports();
|
|
24801
24897
|
}
|
|
24802
24898
|
this.logSeekReady({
|
|
24899
|
+
dragId,
|
|
24803
24900
|
epochMs,
|
|
24804
24901
|
startedAt: seekStartedAt,
|
|
24805
24902
|
locateMs,
|
|
@@ -24815,14 +24912,16 @@ var RecordedFeeder = class {
|
|
|
24815
24912
|
loadAusMs: Math.round(loadAusMs)
|
|
24816
24913
|
});
|
|
24817
24914
|
this.startIdleWarm(this.cursorMs);
|
|
24915
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "served", "paused");
|
|
24818
24916
|
return;
|
|
24819
24917
|
}
|
|
24820
24918
|
if (!stateAnnounced) {
|
|
24821
|
-
this.
|
|
24919
|
+
this.emitState("playing", "seek-landed-playing", token, dragId);
|
|
24822
24920
|
this.deps.onPosition(this.cursorMs);
|
|
24823
24921
|
this.startPositionReports();
|
|
24824
24922
|
}
|
|
24825
24923
|
this.logSeekReady({
|
|
24924
|
+
dragId,
|
|
24826
24925
|
epochMs,
|
|
24827
24926
|
startedAt: seekStartedAt,
|
|
24828
24927
|
locateMs,
|
|
@@ -24852,22 +24951,26 @@ var RecordedFeeder = class {
|
|
|
24852
24951
|
resumeAfterPtsMs
|
|
24853
24952
|
});
|
|
24854
24953
|
this.startIdleWarm(this.cursorMs);
|
|
24954
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "served", "playing");
|
|
24855
24955
|
} catch (err) {
|
|
24856
24956
|
if (!this.disposed && token === this.seekToken) {
|
|
24857
24957
|
this.stopPositionReports();
|
|
24858
24958
|
if (snapInfo !== null) {
|
|
24859
24959
|
this.deps.onLog?.("recorded:seek-snap-load-failed", {
|
|
24960
|
+
dragId,
|
|
24860
24961
|
requestedMs: snapInfo.requestedMs,
|
|
24861
24962
|
snappedMs: snapInfo.snappedMs,
|
|
24862
24963
|
error: require_dist.errMsg(err)
|
|
24863
24964
|
});
|
|
24864
24965
|
this.currentSegment = null;
|
|
24865
24966
|
this.lastFedVideoIdx = -1;
|
|
24866
|
-
this.
|
|
24967
|
+
this.emitState("gap", "seek-snap-load-failed", token, dragId);
|
|
24968
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "gap", "gap");
|
|
24867
24969
|
return;
|
|
24868
24970
|
}
|
|
24869
|
-
this.
|
|
24870
|
-
|
|
24971
|
+
this.emitState("error", "seek-error", token, dragId);
|
|
24972
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "error", "error");
|
|
24973
|
+
} else this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24871
24974
|
} finally {
|
|
24872
24975
|
if (token === this.seekToken) {
|
|
24873
24976
|
this.inFlightSeekEpochMs = null;
|
|
@@ -24877,6 +24980,7 @@ var RecordedFeeder = class {
|
|
|
24877
24980
|
}
|
|
24878
24981
|
logSeekReady(fields) {
|
|
24879
24982
|
this.deps.onLog?.("recorded:seek-ready", {
|
|
24983
|
+
dragId: fields.dragId,
|
|
24880
24984
|
epochMs: fields.epochMs,
|
|
24881
24985
|
state: fields.state,
|
|
24882
24986
|
totalMs: Math.round(performance.now() - fields.startedAt),
|
|
@@ -24911,7 +25015,7 @@ var RecordedFeeder = class {
|
|
|
24911
25015
|
if (next === 0) {
|
|
24912
25016
|
this.advanceToken++;
|
|
24913
25017
|
this.stopPositionReports();
|
|
24914
|
-
this.
|
|
25018
|
+
this.emitState("paused", "rate-zero", this.seekToken, this.scrubDragId);
|
|
24915
25019
|
return;
|
|
24916
25020
|
}
|
|
24917
25021
|
this.pendingStepDirs.length = 0;
|
|
@@ -24933,9 +25037,16 @@ var RecordedFeeder = class {
|
|
|
24933
25037
|
* (which flips `scrubbing` false and clears the session cache) or `dispose()`;
|
|
24934
25038
|
* a genuinely NEW drag is therefore always preceded by that exit, so its
|
|
24935
25039
|
* `beginScrub` runs the full reset below. (D-scrub-ux)
|
|
25040
|
+
*
|
|
25041
|
+
* `dragId` is the client's id for this gesture (optional — the viewer's main
|
|
25042
|
+
* session generates it; a caller that omits it gets identical behaviour,
|
|
25043
|
+
* just no id on the logs). Stored for the whole drag, including its commit:
|
|
25044
|
+
* every `scrub-feeder:*` line printed until the next `beginScrub` carries it
|
|
25045
|
+
* (C1).
|
|
24936
25046
|
*/
|
|
24937
|
-
beginScrub() {
|
|
25047
|
+
beginScrub(dragId) {
|
|
24938
25048
|
if (this.disposed || this.scrubbing) return;
|
|
25049
|
+
this.scrubDragId = dragId;
|
|
24939
25050
|
this.seekToken++;
|
|
24940
25051
|
this.advanceToken++;
|
|
24941
25052
|
this.scrubbing = true;
|
|
@@ -24944,7 +25055,6 @@ var RecordedFeeder = class {
|
|
|
24944
25055
|
this.scrubSegment = null;
|
|
24945
25056
|
this.scrubCredit = SCRUB_CREDIT_CAP;
|
|
24946
25057
|
this.scrubPrevTarget = null;
|
|
24947
|
-
this.scrubDirRun = 0;
|
|
24948
25058
|
this.scrubPrefetched = /* @__PURE__ */ new Set();
|
|
24949
25059
|
this.scrubCursor = null;
|
|
24950
25060
|
this.scrubNoFootageLogged = false;
|
|
@@ -25027,6 +25137,7 @@ var RecordedFeeder = class {
|
|
|
25027
25137
|
this.scrubWarmBackMs = opts?.backMs ?? SCRUB_WARM_BACK_MS;
|
|
25028
25138
|
this.scrubWarmFwdMs = opts?.fwdMs ?? SCRUB_WARM_FWD_MS;
|
|
25029
25139
|
this.deps.onLog?.("scrub-feeder:warm-start", {
|
|
25140
|
+
dragId: this.scrubDragId,
|
|
25030
25141
|
centreMs,
|
|
25031
25142
|
fillGen: this.scrubWarmGen
|
|
25032
25143
|
});
|
|
@@ -25052,6 +25163,7 @@ var RecordedFeeder = class {
|
|
|
25052
25163
|
lastWorked = gen;
|
|
25053
25164
|
if (gen === this.scrubWarmGen) {
|
|
25054
25165
|
this.deps.onLog?.("scrub-feeder:warm-done", {
|
|
25166
|
+
dragId: this.scrubDragId,
|
|
25055
25167
|
fillGen: gen,
|
|
25056
25168
|
idle: this.idleWarm && !this.scrubbing,
|
|
25057
25169
|
fetched,
|
|
@@ -25088,6 +25200,19 @@ var RecordedFeeder = class {
|
|
|
25088
25200
|
if (Math.abs(targetMs - centre) <= SCRUB_RECENTER_MS) return;
|
|
25089
25201
|
this.recenterScrubWarm(targetMs);
|
|
25090
25202
|
}
|
|
25203
|
+
/**
|
|
25204
|
+
* Is the operator's finger actually moving right now?
|
|
25205
|
+
*
|
|
25206
|
+
* `scrubbing` alone is not the question — a drag stays open while the finger
|
|
25207
|
+
* rests, and those pauses are exactly when the warm should work. The signal
|
|
25208
|
+
* is the pointer sample clock, which `noteScrubSample` stamps on every
|
|
25209
|
+
* `scrubTo` before any velocity gating, so it is true even for a drag too
|
|
25210
|
+
* slow or too jittery for the velocity estimator.
|
|
25211
|
+
*/
|
|
25212
|
+
fingerIsMoving() {
|
|
25213
|
+
if (!this.scrubbing) return false;
|
|
25214
|
+
return performance.now() - this.scrubLastSampleAtMs < SCRUB_WARM_QUIET_MS;
|
|
25215
|
+
}
|
|
25091
25216
|
async warmScrubWindow(centreMs, dragGen, fillGen, backMs, fwdMs) {
|
|
25092
25217
|
const spent = await this.warmScrubDirection(centreMs, -1, backMs, 0, dragGen, fillGen);
|
|
25093
25218
|
return this.warmScrubDirection(centreMs, 1, fwdMs, spent, dragGen, fillGen);
|
|
@@ -25127,6 +25252,10 @@ var RecordedFeeder = class {
|
|
|
25127
25252
|
durMs: warm.durMs,
|
|
25128
25253
|
fetched: false
|
|
25129
25254
|
};
|
|
25255
|
+
while (this.fingerIsMoving()) {
|
|
25256
|
+
if (!this.warmLoopAlive(dragGen) || fillGen !== this.scrubWarmGen) return null;
|
|
25257
|
+
await new Promise((r) => setTimeout(r, SCRUB_WARM_QUIET_MS));
|
|
25258
|
+
}
|
|
25130
25259
|
if (this.idleWarm && !this.scrubbing) {
|
|
25131
25260
|
await new Promise((r) => setTimeout(r, this.deps.idleWarmGapMs ?? IDLE_WARM_READ_GAP_MS));
|
|
25132
25261
|
while (this.inFlightSeekEpochMs !== null) {
|
|
@@ -25146,7 +25275,7 @@ var RecordedFeeder = class {
|
|
|
25146
25275
|
if (loc.kind !== "segment") return null;
|
|
25147
25276
|
if (!await this.waitForTickReadIdle(fillGen)) return null;
|
|
25148
25277
|
const load = await this.loadScrubFragment(loc, probeMs);
|
|
25149
|
-
if (load !== null && this.scrubbing) this.scrubStats?.
|
|
25278
|
+
if (load !== null && this.scrubbing) this.scrubStats?.recordWarmRead(load.readBytes);
|
|
25150
25279
|
if (!this.warmLoopAlive(dragGen) || load === null) return null;
|
|
25151
25280
|
if (fillGen !== this.scrubWarmGen) return null;
|
|
25152
25281
|
this.scrubCache.put(this.deps.profile, load.seg);
|
|
@@ -25266,6 +25395,7 @@ var RecordedFeeder = class {
|
|
|
25266
25395
|
if (!this.scrubAckTimeoutLogged) {
|
|
25267
25396
|
this.scrubAckTimeoutLogged = true;
|
|
25268
25397
|
this.deps.onLog?.("scrub-feeder:ack-timeout", {
|
|
25398
|
+
dragId: this.scrubDragId,
|
|
25269
25399
|
pushes: expired,
|
|
25270
25400
|
frames,
|
|
25271
25401
|
timeoutMs: SCRUB_ACK_TIMEOUT_MS,
|
|
@@ -25297,6 +25427,7 @@ var RecordedFeeder = class {
|
|
|
25297
25427
|
if (!this.scrubParkLogged) {
|
|
25298
25428
|
this.scrubParkLogged = true;
|
|
25299
25429
|
this.deps.onLog?.("scrub-feeder:parked", {
|
|
25430
|
+
dragId: this.scrubDragId,
|
|
25300
25431
|
pendingEpochMs: this.scrubPendingEpochMs,
|
|
25301
25432
|
outstanding: this.scrubOutstanding.length
|
|
25302
25433
|
});
|
|
@@ -25329,7 +25460,7 @@ var RecordedFeeder = class {
|
|
|
25329
25460
|
seg = this.scrubCache.covering(this.deps.profile, target);
|
|
25330
25461
|
if (seg) {
|
|
25331
25462
|
coverage = "cache";
|
|
25332
|
-
if (this.scrubPrefetched.has(seg.startMs)) this.scrubStats?.
|
|
25463
|
+
if (this.scrubPrefetched.has(seg.startMs)) this.scrubStats?.recordWarmHit();
|
|
25333
25464
|
}
|
|
25334
25465
|
}
|
|
25335
25466
|
if (!seg) {
|
|
@@ -25359,6 +25490,7 @@ var RecordedFeeder = class {
|
|
|
25359
25490
|
if (!this.scrubDeadlineLogged) {
|
|
25360
25491
|
this.scrubDeadlineLogged = true;
|
|
25361
25492
|
this.deps.onLog?.("scrub-feeder:deadline-miss", {
|
|
25493
|
+
dragId: this.scrubDragId,
|
|
25362
25494
|
epochMs: target,
|
|
25363
25495
|
deadlineMs,
|
|
25364
25496
|
first: !this.scrubPushedThisDrag,
|
|
@@ -25482,6 +25614,7 @@ var RecordedFeeder = class {
|
|
|
25482
25614
|
if (this.scrubLastPushLogAtMs === 0 || pushedAtMs - this.scrubLastPushLogAtMs >= 1e3) {
|
|
25483
25615
|
this.scrubLastPushLogAtMs = pushedAtMs;
|
|
25484
25616
|
this.deps.onLog?.("scrub-feeder:pushed", {
|
|
25617
|
+
dragId: this.scrubDragId,
|
|
25485
25618
|
epochMs: effTarget,
|
|
25486
25619
|
mode,
|
|
25487
25620
|
dir,
|
|
@@ -25499,10 +25632,11 @@ var RecordedFeeder = class {
|
|
|
25499
25632
|
});
|
|
25500
25633
|
}
|
|
25501
25634
|
this.deps.onPosition(this.clampIntoSegment(seg, effTarget), pushSeq);
|
|
25502
|
-
this.
|
|
25635
|
+
this.scrubPrevTarget = effTarget;
|
|
25503
25636
|
} catch (err) {
|
|
25504
25637
|
this.scrubStats?.recordFailure();
|
|
25505
25638
|
this.deps.onLog?.("scrub-feeder:tick-failed", {
|
|
25639
|
+
dragId: this.scrubDragId,
|
|
25506
25640
|
epochMs: target,
|
|
25507
25641
|
error: err instanceof Error ? err.message : String(err)
|
|
25508
25642
|
});
|
|
@@ -25548,11 +25682,15 @@ var RecordedFeeder = class {
|
|
|
25548
25682
|
if (!this.scrubNoFootageLogged) {
|
|
25549
25683
|
this.scrubNoFootageLogged = true;
|
|
25550
25684
|
if (decision.kind === "too-far") this.deps.onLog?.("scrub-feeder:snap-too-far", {
|
|
25685
|
+
dragId: this.scrubDragId,
|
|
25551
25686
|
epochMs: target,
|
|
25552
25687
|
nearestMs: decision.snapMs,
|
|
25553
25688
|
distanceMs: decision.distanceMs
|
|
25554
25689
|
});
|
|
25555
|
-
else this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25690
|
+
else this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25691
|
+
dragId: this.scrubDragId,
|
|
25692
|
+
epochMs: target
|
|
25693
|
+
});
|
|
25556
25694
|
}
|
|
25557
25695
|
return null;
|
|
25558
25696
|
}
|
|
@@ -25568,11 +25706,15 @@ var RecordedFeeder = class {
|
|
|
25568
25706
|
if (snapped.kind !== "segment") {
|
|
25569
25707
|
if (!this.scrubNoFootageLogged) {
|
|
25570
25708
|
this.scrubNoFootageLogged = true;
|
|
25571
|
-
this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25709
|
+
this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25710
|
+
dragId: this.scrubDragId,
|
|
25711
|
+
epochMs: target
|
|
25712
|
+
});
|
|
25572
25713
|
}
|
|
25573
25714
|
return null;
|
|
25574
25715
|
}
|
|
25575
25716
|
this.deps.onLog?.("scrub-feeder:snapped", {
|
|
25717
|
+
dragId: this.scrubDragId,
|
|
25576
25718
|
requestedMs: target,
|
|
25577
25719
|
snappedMs: decision.snapMs
|
|
25578
25720
|
});
|
|
@@ -25705,11 +25847,12 @@ var RecordedFeeder = class {
|
|
|
25705
25847
|
*/
|
|
25706
25848
|
readsWholeSegment(loc) {
|
|
25707
25849
|
if (this.deps.profile !== SCRUB_WHOLE_SEGMENT_PROFILE) return false;
|
|
25708
|
-
if (loc.bytes === void 0 || loc.bytes <=
|
|
25850
|
+
if (loc.bytes === void 0 || loc.bytes <= 15e5) return true;
|
|
25709
25851
|
this.scrubStats?.recordOversizeSegment();
|
|
25710
25852
|
if (!this.scrubOversizeLogged) {
|
|
25711
25853
|
this.scrubOversizeLogged = true;
|
|
25712
25854
|
this.deps.onLog?.("scrub-feeder:oversize-segment", {
|
|
25855
|
+
dragId: this.scrubDragId,
|
|
25713
25856
|
profile: this.deps.profile,
|
|
25714
25857
|
startMs: loc.startMs,
|
|
25715
25858
|
durMs: loc.durMs,
|
|
@@ -25745,6 +25888,7 @@ var RecordedFeeder = class {
|
|
|
25745
25888
|
if (this.scrubProfileLogged) return;
|
|
25746
25889
|
this.scrubProfileLogged = true;
|
|
25747
25890
|
this.deps.onLog?.("scrub-feeder:profile-degraded", {
|
|
25891
|
+
dragId: this.scrubDragId,
|
|
25748
25892
|
profile: this.deps.profile,
|
|
25749
25893
|
scrubProfile: SCRUB_WHOLE_SEGMENT_PROFILE
|
|
25750
25894
|
});
|
|
@@ -25760,17 +25904,6 @@ var RecordedFeeder = class {
|
|
|
25760
25904
|
return sorted[Math.floor(sorted.length / 2)] ?? null;
|
|
25761
25905
|
}
|
|
25762
25906
|
/**
|
|
25763
|
-
* Is the disk the problem right now? The recent-read median against
|
|
25764
|
-
* {@link SCRUB_SLOW_READ_MS}. Median and not mean because one slow read is
|
|
25765
|
-
* noise and the decision must not swing on it; and never before a few
|
|
25766
|
-
* samples, or a cold first read would suppress the whole drag.
|
|
25767
|
-
*/
|
|
25768
|
-
scrubReadsAreSlow() {
|
|
25769
|
-
const median = this.scrubReadMedianMs();
|
|
25770
|
-
if (median === null) return false;
|
|
25771
|
-
return median >= (this.deps.slowReadMs ?? SCRUB_SLOW_READ_MS);
|
|
25772
|
-
}
|
|
25773
|
-
/**
|
|
25774
25907
|
* Fold one pointer sample into the drag's media velocity (ms of timeline per
|
|
25775
25908
|
* ms of wall clock, signed). Cheap on purpose: this runs at pointer rate.
|
|
25776
25909
|
*/
|
|
@@ -25806,7 +25939,7 @@ var RecordedFeeder = class {
|
|
|
25806
25939
|
if (Math.sign(velocity) !== Math.sign(this.scrubVelRun)) return 0;
|
|
25807
25940
|
const readMs = this.scrubReadMedianMs();
|
|
25808
25941
|
if (readMs === null) return 0;
|
|
25809
|
-
return
|
|
25942
|
+
return scrubLeadDecision(velocity, readMs).leadMs;
|
|
25810
25943
|
}
|
|
25811
25944
|
/**
|
|
25812
25945
|
* A single slow `locate` gets its own line, throttled per drag. Reserved for
|
|
@@ -25822,6 +25955,7 @@ var RecordedFeeder = class {
|
|
|
25822
25955
|
if (this.scrubLastSlowLocateLogAtMs !== 0 && now - this.scrubLastSlowLocateLogAtMs < SCRUB_PATHOLOGY_LOG_INTERVAL_MS) return;
|
|
25823
25956
|
this.scrubLastSlowLocateLogAtMs = now;
|
|
25824
25957
|
this.deps.onLog?.("scrub-feeder:slow-locate", {
|
|
25958
|
+
dragId: this.scrubDragId,
|
|
25825
25959
|
epochMs,
|
|
25826
25960
|
locateMs: Math.round(locateMs),
|
|
25827
25961
|
thresholdMs: threshold
|
|
@@ -25838,7 +25972,7 @@ var RecordedFeeder = class {
|
|
|
25838
25972
|
* interval comes out warm for the next drag. Aborts (without landing — the
|
|
25839
25973
|
* caller owns that) the moment a newer seek bumps the token.
|
|
25840
25974
|
*/
|
|
25841
|
-
async animateSeekTravel(fromMs, toMs, token) {
|
|
25975
|
+
async animateSeekTravel(fromMs, toMs, token, dragId) {
|
|
25842
25976
|
const cfg = this.deps.seekAnimation;
|
|
25843
25977
|
const budgetMs = cfg?.budgetMs ?? SEEK_ANIMATION_BUDGET_MS;
|
|
25844
25978
|
const tickMs = cfg?.tickMs ?? SEEK_ANIMATION_TICK_MS;
|
|
@@ -25870,6 +26004,7 @@ var RecordedFeeder = class {
|
|
|
25870
26004
|
if (this.disposed || token !== this.seekToken) return;
|
|
25871
26005
|
}
|
|
25872
26006
|
if (stills > 0) this.deps.onLog?.("recorded:seek-travel", {
|
|
26007
|
+
dragId,
|
|
25873
26008
|
fromMs,
|
|
25874
26009
|
toMs,
|
|
25875
26010
|
stills,
|
|
@@ -25964,79 +26099,41 @@ var RecordedFeeder = class {
|
|
|
25964
26099
|
return stills;
|
|
25965
26100
|
}
|
|
25966
26101
|
/**
|
|
25967
|
-
*
|
|
25968
|
-
* shows a sustained direction, kick off a single background prefetch of the
|
|
25969
|
-
* NEXT fragment in that direction. Prefetch fills the cache only — it never
|
|
25970
|
-
* pushes, so crossing into the neighbour later is an instant cache hit.
|
|
25971
|
-
*/
|
|
25972
|
-
maybeScrubPrefetch(seg, target, gen) {
|
|
25973
|
-
const prev = this.scrubPrevTarget;
|
|
25974
|
-
this.scrubPrevTarget = target;
|
|
25975
|
-
if (prev === null) {
|
|
25976
|
-
this.scrubDirRun = 0;
|
|
25977
|
-
return;
|
|
25978
|
-
}
|
|
25979
|
-
const stepMs = target - prev;
|
|
25980
|
-
const dir = stepMs > 0 ? 1 : stepMs < 0 ? -1 : 0;
|
|
25981
|
-
if (dir === 0) {
|
|
25982
|
-
this.scrubDirRun = 0;
|
|
25983
|
-
return;
|
|
25984
|
-
}
|
|
25985
|
-
this.scrubDirRun = Math.sign(this.scrubDirRun) === dir ? this.scrubDirRun + dir : dir;
|
|
25986
|
-
if (Math.abs(this.scrubDirRun) < SCRUB_PREFETCH_DIR_THRESHOLD) return;
|
|
25987
|
-
if (this.scrubReadsAreSlow()) {
|
|
25988
|
-
this.scrubStats?.recordPrefetchSuppressed();
|
|
25989
|
-
return;
|
|
25990
|
-
}
|
|
25991
|
-
if (this.scrubFillLoopActive) return;
|
|
25992
|
-
const aheadMs = Math.max(Math.abs(stepMs), seg.durMs + 1);
|
|
25993
|
-
const horizons = Math.abs(stepMs) > seg.durMs ? SCRUB_PREFETCH_MAX_INFLIGHT : 1;
|
|
25994
|
-
for (let horizon = 1; horizon <= horizons; horizon++) {
|
|
25995
|
-
if (this.scrubPrefetchInFlight >= SCRUB_PREFETCH_MAX_INFLIGHT) return;
|
|
25996
|
-
const probeMs = target + dir * aheadMs * horizon;
|
|
25997
|
-
if (probeMs < 0) continue;
|
|
25998
|
-
if (this.scrubPrefetchProbes.has(probeMs)) continue;
|
|
25999
|
-
if (this.scrubCache.covering(this.deps.profile, probeMs)) continue;
|
|
26000
|
-
if (this.scrubWarmCentreMs !== null && Math.abs(probeMs - this.scrubWarmCentreMs) > SCRUB_WARM_AROUND_MS) continue;
|
|
26001
|
-
this.scrubPrefetch(probeMs, gen);
|
|
26002
|
-
}
|
|
26003
|
-
}
|
|
26004
|
-
/**
|
|
26005
|
-
* Speculative read: locate + read + demux the fragment covering `probeMs` into
|
|
26006
|
-
* `scrubCache`. Bounded by {@link SCRUB_PREFETCH_MAX_INFLIGHT}; aborts at any
|
|
26007
|
-
* teardown (`disposed` / left scrub / a bumped `scrubToken`); a failure is
|
|
26008
|
-
* swallowed (the fragment just demuxes on demand). Never pushes and never
|
|
26009
|
-
* mutates latest-wins state.
|
|
26102
|
+
* The directional prefetch is GONE (2026-08-26), not disabled.
|
|
26010
26103
|
*
|
|
26011
|
-
*
|
|
26012
|
-
*
|
|
26013
|
-
*
|
|
26014
|
-
*
|
|
26015
|
-
|
|
26016
|
-
|
|
26017
|
-
|
|
26018
|
-
|
|
26019
|
-
|
|
26020
|
-
|
|
26021
|
-
|
|
26022
|
-
|
|
26023
|
-
|
|
26024
|
-
|
|
26025
|
-
|
|
26026
|
-
|
|
26027
|
-
|
|
26028
|
-
|
|
26029
|
-
|
|
26030
|
-
|
|
26031
|
-
|
|
26032
|
-
|
|
26033
|
-
|
|
26034
|
-
|
|
26035
|
-
|
|
26036
|
-
|
|
26037
|
-
|
|
26038
|
-
|
|
26039
|
-
|
|
26104
|
+
* It guessed where the next tick would land from the drag's recent
|
|
26105
|
+
* direction. Measured on the operator's own drags the day it was removed:
|
|
26106
|
+
* **25 speculative reads bought 3 hits** — 88% waste, and roughly 60% of all
|
|
26107
|
+
* disk work during a gesture, queued as random seeks ahead of the read the
|
|
26108
|
+
* finger was actually blocked on, on a 5400 rpm SMR disk.
|
|
26109
|
+
*
|
|
26110
|
+
* The regression tests it passed drove a drag at CONSTANT step size, which is
|
|
26111
|
+
* the one regime where direction predicts position. A real backward scrub is
|
|
26112
|
+
* exploratory — 26 of 29 jumps backward, step size set by where the operator
|
|
26113
|
+
* decided to look next — so the premise never held in the field.
|
|
26114
|
+
*
|
|
26115
|
+
* Scrypted has no predictive prefetch either: their read-ahead is a stream
|
|
26116
|
+
* `highWaterMark`, and a short forward move is a fast-forward on an open fd
|
|
26117
|
+
* rather than a seek. They deleted the problem instead of solving it.
|
|
26118
|
+
*
|
|
26119
|
+
* What REMAINS and must not be confused with this: the WINDOW warm
|
|
26120
|
+
* (`runScrubFillLoop`) — a sequential walk of the window around the playhead.
|
|
26121
|
+
*
|
|
26122
|
+
* It was described here, wrongly, as running "when no drag is live, so it
|
|
26123
|
+
* competes with nothing". It does not: its survival condition is
|
|
26124
|
+
* `this.scrubbing || this.idleWarm`, and within hours of this deletion it had
|
|
26125
|
+
* inherited the deleted predictor's role — 1.9 MB and 2.4 MB of reads on two
|
|
26126
|
+
* measured drags, 38-45% of the gesture's I/O, zero cache hits. Deleting one
|
|
26127
|
+
* speculator without looking at the other left the contention exactly where
|
|
26128
|
+
* it was. It now waits out a moving finger (`fingerIsMoving`), which is what
|
|
26129
|
+
* the sentence above claimed all along.
|
|
26130
|
+
*
|
|
26131
|
+
* Cost of the removal, bounded and accepted: a careful short-step drag no
|
|
26132
|
+
* longer warms the fragment across the edge, so the first tick past a
|
|
26133
|
+
* boundary may pay one cold read. Its own regression test shows that case is
|
|
26134
|
+
* served by the MEMO (`coverMemo ≥ 5`) with the neighbour warm worth ≤2
|
|
26135
|
+
* reads.
|
|
26136
|
+
*/
|
|
26040
26137
|
/**
|
|
26041
26138
|
* Feed exactly one frame forward (+1) or backward (-1). Only meaningful while
|
|
26042
26139
|
* paused over a segment (rate 0, recorded mode) — ignored otherwise. Forward
|
|
@@ -26220,7 +26317,7 @@ var RecordedFeeder = class {
|
|
|
26220
26317
|
this.advanceLoop(gen, initial).catch(() => {
|
|
26221
26318
|
if (!this.disposed && gen === this.advanceToken) {
|
|
26222
26319
|
this.stopPositionReports();
|
|
26223
|
-
this.
|
|
26320
|
+
this.emitState("error", "advance-loop-failed", this.seekToken, this.scrubDragId);
|
|
26224
26321
|
}
|
|
26225
26322
|
});
|
|
26226
26323
|
}
|
|
@@ -26237,7 +26334,7 @@ var RecordedFeeder = class {
|
|
|
26237
26334
|
if (prepared === null) return;
|
|
26238
26335
|
if (prepared === "ended") {
|
|
26239
26336
|
this.stopPositionReports();
|
|
26240
|
-
this.
|
|
26337
|
+
this.emitState("ended", "advance-ended", this.seekToken, this.scrubDragId);
|
|
26241
26338
|
return;
|
|
26242
26339
|
}
|
|
26243
26340
|
if (isResumedSegment) {} else {
|
|
@@ -26514,7 +26611,10 @@ var RecordedFeeder = class {
|
|
|
26514
26611
|
const mark = this.scrubDemuxMark;
|
|
26515
26612
|
const demux = mark && this.deps.demuxTally ? this.deps.demuxTally.deltaSince(mark) : void 0;
|
|
26516
26613
|
this.scrubDemuxMark = null;
|
|
26517
|
-
this.deps.onLog?.("scrub-feeder:drag-end",
|
|
26614
|
+
this.deps.onLog?.("scrub-feeder:drag-end", {
|
|
26615
|
+
dragId: this.scrubDragId,
|
|
26616
|
+
...stats.toLogFields(performance.now() - this.scrubStartedAtMs, demux)
|
|
26617
|
+
});
|
|
26518
26618
|
}
|
|
26519
26619
|
/** Stop everything: invalidate in-flight work and tear down the position timer. */
|
|
26520
26620
|
dispose() {
|
|
@@ -26527,7 +26627,6 @@ var RecordedFeeder = class {
|
|
|
26527
26627
|
this.scrubToken++;
|
|
26528
26628
|
this.scrubSegment = null;
|
|
26529
26629
|
this.scrubPrevTarget = null;
|
|
26530
|
-
this.scrubDirRun = 0;
|
|
26531
26630
|
this.scrubCursor = null;
|
|
26532
26631
|
this.scrubNoFootageLogged = false;
|
|
26533
26632
|
this.scrubParkLogged = false;
|
|
@@ -26619,10 +26718,11 @@ var TimelineSession = class {
|
|
|
26619
26718
|
await this.playAt(msg.epoch, this.profile, { travelFromMs: this.lastPositionMs });
|
|
26620
26719
|
break;
|
|
26621
26720
|
case "scrubCommit":
|
|
26622
|
-
await this.playAt(msg.epoch, this.profile);
|
|
26721
|
+
if (msg.dragId !== void 0) await this.playAt(msg.epoch, this.profile, { dragId: msg.dragId });
|
|
26722
|
+
else await this.playAt(msg.epoch, this.profile);
|
|
26623
26723
|
break;
|
|
26624
26724
|
case "scrubStart":
|
|
26625
|
-
(this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).beginScrub();
|
|
26725
|
+
(this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).beginScrub(msg.dragId);
|
|
26626
26726
|
break;
|
|
26627
26727
|
case "scrubTo": {
|
|
26628
26728
|
const hint = msg.m !== void 0 && msg.v !== void 0 ? {
|
|
@@ -26876,7 +26976,7 @@ function createOffThreadJsParse(deps) {
|
|
|
26876
26976
|
if (!w) return deps.parseInProcess(bytes);
|
|
26877
26977
|
const id = nextId;
|
|
26878
26978
|
nextId += 1;
|
|
26879
|
-
const copy = bytes
|
|
26979
|
+
const copy = new Uint8Array(bytes);
|
|
26880
26980
|
const request = {
|
|
26881
26981
|
id,
|
|
26882
26982
|
bytes: copy
|
|
@@ -28313,14 +28413,21 @@ var BrokerWebrtcServer = class {
|
|
|
28313
28413
|
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
28314
28414
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
28315
28415
|
},
|
|
28316
|
-
onState: (
|
|
28317
|
-
sessionLogger.info("recorded: feeder state", {
|
|
28318
|
-
|
|
28319
|
-
|
|
28320
|
-
|
|
28321
|
-
|
|
28322
|
-
|
|
28323
|
-
|
|
28416
|
+
onState: (t) => {
|
|
28417
|
+
sessionLogger.info("recorded: feeder state", {
|
|
28418
|
+
tags: { deviceId },
|
|
28419
|
+
meta: {
|
|
28420
|
+
sessionId,
|
|
28421
|
+
deviceId,
|
|
28422
|
+
profile,
|
|
28423
|
+
from: t.from,
|
|
28424
|
+
to: t.to,
|
|
28425
|
+
reason: t.reason,
|
|
28426
|
+
seekToken: t.seekToken,
|
|
28427
|
+
dragId: t.dragId
|
|
28428
|
+
}
|
|
28429
|
+
});
|
|
28430
|
+
timeline._emitState(t.to);
|
|
28324
28431
|
},
|
|
28325
28432
|
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
28326
28433
|
onLog: (msg, data) => sessionLogger.info(msg, {
|