@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { At as makeSourceBrokerId, C as RATE_CONTROL_TIGHT, Ct as CAM_PROFILE_ORDER, E as RingBuffer, Et as createEvent, Gt as string, H as egressTranscodeSharingKey, Ht as number, It as _enum, Kt as union, Lt as array, M as batteryCapability, N as cameraStreamsCapability, Nt as parseProfileBrokerId, O as addonWidgetsSourceCapability, Ot as isEvent, P as createHwAccelCache, R as deriveBatteryPresence, Rt as boolean, S as RATE_CONTROL_RELAXED, St as BaseAddon, Tt as DeviceType, U as egressTransportFromRequest, Ut as object, Vt as literal, Wt as record, Y as maskUrlCredentials, _t as AUDIO_PRESETS, bt as invocationFromEncodeProfile, ct as resolveEgressDecodeHwAccel, gt as errMsg, h as EncodeProfileSchema, ht as webrtcSessionCapability, i as BatteryStatusSchema, jt as nodePin, kt as makeProfileBrokerId, pt as streamBrokerCapability, qt as EventCategory, vt as Fmp4BoxSplitter, wt as DeviceFeature, xt as isSoftwareDecode, yt as buildFfmpegArgs$1, zt as discriminatedUnion } from "../dist-
|
|
2
|
+
import { At as makeSourceBrokerId, C as RATE_CONTROL_TIGHT, Ct as CAM_PROFILE_ORDER, E as RingBuffer, Et as createEvent, Gt as string, H as egressTranscodeSharingKey, Ht as number, It as _enum, Kt as union, Lt as array, M as batteryCapability, N as cameraStreamsCapability, Nt as parseProfileBrokerId, O as addonWidgetsSourceCapability, Ot as isEvent, P as createHwAccelCache, R as deriveBatteryPresence, Rt as boolean, S as RATE_CONTROL_RELAXED, St as BaseAddon, Tt as DeviceType, U as egressTransportFromRequest, Ut as object, Vt as literal, Wt as record, Y as maskUrlCredentials, _t as AUDIO_PRESETS, bt as invocationFromEncodeProfile, ct as resolveEgressDecodeHwAccel, gt as errMsg, h as EncodeProfileSchema, ht as webrtcSessionCapability, i as BatteryStatusSchema, jt as nodePin, kt as makeProfileBrokerId, pt as streamBrokerCapability, qt as EventCategory, vt as Fmp4BoxSplitter, wt as DeviceFeature, xt as isSoftwareDecode, yt as buildFfmpegArgs$1, zt as discriminatedUnion } from "../dist-DO0UVhOk.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";
|
|
6
|
-
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
6
|
+
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-skhsAZx6.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import * as crypto$1 from "node:crypto";
|
|
9
9
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -23474,9 +23474,13 @@ var ControlMessageSchema = discriminatedUnion("t", [
|
|
|
23474
23474
|
}),
|
|
23475
23475
|
object({
|
|
23476
23476
|
t: literal("scrubCommit"),
|
|
23477
|
-
epoch: number()
|
|
23477
|
+
epoch: number(),
|
|
23478
|
+
dragId: string().optional()
|
|
23479
|
+
}),
|
|
23480
|
+
object({
|
|
23481
|
+
t: literal("scrubStart"),
|
|
23482
|
+
dragId: string().optional()
|
|
23478
23483
|
}),
|
|
23479
|
-
object({ t: literal("scrubStart") }),
|
|
23480
23484
|
object({
|
|
23481
23485
|
t: literal("scrubTo"),
|
|
23482
23486
|
epoch: number(),
|
|
@@ -23628,104 +23632,6 @@ var MonotonicClock = class {
|
|
|
23628
23632
|
this.rate = rate;
|
|
23629
23633
|
}
|
|
23630
23634
|
};
|
|
23631
|
-
var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 48 * 1024 * 1024;
|
|
23632
|
-
var SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES = 2 * 1024 * 1024;
|
|
23633
|
-
/** Demuxed bytes an entry holds — what the byte bound actually counts. */
|
|
23634
|
-
function fragmentBytes(frag) {
|
|
23635
|
-
let total = 0;
|
|
23636
|
-
for (const au of frag.aus) total += au.data.length;
|
|
23637
|
-
return total;
|
|
23638
|
-
}
|
|
23639
|
-
var ScrubFragmentCache = class {
|
|
23640
|
-
maxBytes;
|
|
23641
|
-
otherMaxBytes;
|
|
23642
|
-
map = /* @__PURE__ */ new Map();
|
|
23643
|
-
/**
|
|
23644
|
-
* Bytes held PER PROFILE. Budgets are per profile and so is eviction: a
|
|
23645
|
-
* `high` entry must never be able to evict the `low` segments the drag is
|
|
23646
|
-
* running on, and the reverse would be just as wrong.
|
|
23647
|
-
*/
|
|
23648
|
-
bytesByProfile = /* @__PURE__ */ new Map();
|
|
23649
|
-
constructor(maxBytes = SCRUB_FRAGMENT_CACHE_MAX_BYTES, otherMaxBytes = SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES) {
|
|
23650
|
-
this.maxBytes = maxBytes;
|
|
23651
|
-
this.otherMaxBytes = otherMaxBytes;
|
|
23652
|
-
}
|
|
23653
|
-
/** The byte budget for a profile — the scrub profile gets the real one. */
|
|
23654
|
-
budgetFor(profile) {
|
|
23655
|
-
return profile === "low" ? this.maxBytes : this.otherMaxBytes;
|
|
23656
|
-
}
|
|
23657
|
-
key(profile, startMs) {
|
|
23658
|
-
return `${profile}|${startMs}`;
|
|
23659
|
-
}
|
|
23660
|
-
/** Exact-start lookup with LRU touch (re-insertion marks most recent). */
|
|
23661
|
-
get(profile, startMs) {
|
|
23662
|
-
const k = this.key(profile, startMs);
|
|
23663
|
-
const frag = this.map.get(k);
|
|
23664
|
-
if (!frag) return null;
|
|
23665
|
-
this.map.delete(k);
|
|
23666
|
-
this.map.set(k, frag);
|
|
23667
|
-
return frag;
|
|
23668
|
-
}
|
|
23669
|
-
/**
|
|
23670
|
-
* The cached fragment COVERING an instant, if any — the lookup that lets a
|
|
23671
|
-
* warm drag tick skip `locate` entirely (an RPC measured at 1–8 s on a
|
|
23672
|
-
* saturated hub). Coverage is proof of footage: recorded fragments are
|
|
23673
|
-
* immutable, so a hit needs no re-validation. LRU-touches the hit.
|
|
23674
|
-
*/
|
|
23675
|
-
covering(profile, epochMs) {
|
|
23676
|
-
for (const [k, frag] of this.map) {
|
|
23677
|
-
if (!k.startsWith(`${profile}|`)) continue;
|
|
23678
|
-
if (epochMs >= frag.startMs && epochMs < frag.startMs + frag.durMs) {
|
|
23679
|
-
this.map.delete(k);
|
|
23680
|
-
this.map.set(k, frag);
|
|
23681
|
-
return frag;
|
|
23682
|
-
}
|
|
23683
|
-
}
|
|
23684
|
-
return null;
|
|
23685
|
-
}
|
|
23686
|
-
/** Insert (or refresh) a fragment, evicting that PROFILE's oldest past its bound. */
|
|
23687
|
-
put(profile, frag) {
|
|
23688
|
-
const k = this.key(profile, frag.startMs);
|
|
23689
|
-
const held = this.map.get(k);
|
|
23690
|
-
let bytes = this.bytesByProfile.get(profile) ?? 0;
|
|
23691
|
-
if (held) bytes -= fragmentBytes(held);
|
|
23692
|
-
this.map.delete(k);
|
|
23693
|
-
this.map.set(k, frag);
|
|
23694
|
-
bytes += fragmentBytes(frag);
|
|
23695
|
-
this.bytesByProfile.set(profile, bytes);
|
|
23696
|
-
const budget = this.budgetFor(profile);
|
|
23697
|
-
const prefix = `${profile}|`;
|
|
23698
|
-
while ((this.bytesByProfile.get(profile) ?? 0) > budget && this.entriesFor(profile) > 4) {
|
|
23699
|
-
const oldest = this.oldestKeyWithPrefix(prefix);
|
|
23700
|
-
if (oldest === void 0) break;
|
|
23701
|
-
const evicted = this.map.get(oldest);
|
|
23702
|
-
this.map.delete(oldest);
|
|
23703
|
-
if (evicted) this.bytesByProfile.set(profile, (this.bytesByProfile.get(profile) ?? 0) - fragmentBytes(evicted));
|
|
23704
|
-
}
|
|
23705
|
-
}
|
|
23706
|
-
oldestKeyWithPrefix(prefix) {
|
|
23707
|
-
for (const k of this.map.keys()) if (k.startsWith(prefix)) return k;
|
|
23708
|
-
}
|
|
23709
|
-
entriesFor(profile) {
|
|
23710
|
-
const prefix = `${profile}|`;
|
|
23711
|
-
let n = 0;
|
|
23712
|
-
for (const k of this.map.keys()) if (k.startsWith(prefix)) n += 1;
|
|
23713
|
-
return n;
|
|
23714
|
-
}
|
|
23715
|
-
/** Demuxed bytes currently held across every profile. */
|
|
23716
|
-
get byteSize() {
|
|
23717
|
-
let total = 0;
|
|
23718
|
-
for (const bytes of this.bytesByProfile.values()) total += bytes;
|
|
23719
|
-
return total;
|
|
23720
|
-
}
|
|
23721
|
-
/** Demuxed bytes held for one profile — the number its MB bound governs. */
|
|
23722
|
-
byteSizeFor(profile) {
|
|
23723
|
-
return this.bytesByProfile.get(profile) ?? 0;
|
|
23724
|
-
}
|
|
23725
|
-
get size() {
|
|
23726
|
-
return this.map.size;
|
|
23727
|
-
}
|
|
23728
|
-
};
|
|
23729
23635
|
/**
|
|
23730
23636
|
* A bounded numeric series. Exact count/sum/min/max forever; the median comes
|
|
23731
23637
|
* from the first {@link SCRUB_PHASE_SAMPLE_CAP} samples.
|
|
@@ -23791,6 +23697,9 @@ var ScrubDragStats = class {
|
|
|
23791
23697
|
prefetchBytes = 0;
|
|
23792
23698
|
prefetchHits = 0;
|
|
23793
23699
|
prefetchSuppressed = 0;
|
|
23700
|
+
warmReads = 0;
|
|
23701
|
+
warmBytes = 0;
|
|
23702
|
+
warmHits = 0;
|
|
23794
23703
|
ackTimeouts = 0;
|
|
23795
23704
|
deadlineMisses = 0;
|
|
23796
23705
|
retargets = 0;
|
|
@@ -23857,6 +23766,15 @@ var ScrubDragStats = class {
|
|
|
23857
23766
|
recordPrefetchHit() {
|
|
23858
23767
|
this.prefetchHits += 1;
|
|
23859
23768
|
}
|
|
23769
|
+
/** One window-warm read taken while the drag was live, and what it cost. */
|
|
23770
|
+
recordWarmRead(bytes) {
|
|
23771
|
+
this.warmReads += 1;
|
|
23772
|
+
this.warmBytes += bytes;
|
|
23773
|
+
}
|
|
23774
|
+
/** A tick served by a fragment the window warm had already landed. */
|
|
23775
|
+
recordWarmHit() {
|
|
23776
|
+
this.warmHits += 1;
|
|
23777
|
+
}
|
|
23860
23778
|
/** Pushes written off after the ack timeout — dropped work, never silent. */
|
|
23861
23779
|
recordAckTimeout(pushes) {
|
|
23862
23780
|
this.ackTimeouts += pushes;
|
|
@@ -23939,6 +23857,9 @@ var ScrubDragStats = class {
|
|
|
23939
23857
|
prefetchBytes: this.prefetchBytes,
|
|
23940
23858
|
prefetchHits: this.prefetchHits,
|
|
23941
23859
|
prefetchSuppressed: this.prefetchSuppressed,
|
|
23860
|
+
warmReads: this.warmReads,
|
|
23861
|
+
warmBytes: this.warmBytes,
|
|
23862
|
+
warmHits: this.warmHits,
|
|
23942
23863
|
ackTimeouts: this.ackTimeouts,
|
|
23943
23864
|
deadlineMisses: this.deadlineMisses,
|
|
23944
23865
|
retargets: this.retargets,
|
|
@@ -23958,8 +23879,158 @@ var ScrubDragStats = class {
|
|
|
23958
23879
|
};
|
|
23959
23880
|
}
|
|
23960
23881
|
};
|
|
23882
|
+
var SCRUB_FRAGMENT_CACHE_MAX_BYTES = 48 * 1024 * 1024;
|
|
23883
|
+
var SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES = 2 * 1024 * 1024;
|
|
23884
|
+
/** Demuxed bytes an entry holds — what the byte bound actually counts. */
|
|
23885
|
+
function fragmentBytes(frag) {
|
|
23886
|
+
let total = 0;
|
|
23887
|
+
for (const au of frag.aus) total += au.data.length;
|
|
23888
|
+
return total;
|
|
23889
|
+
}
|
|
23890
|
+
var ScrubFragmentCache = class {
|
|
23891
|
+
maxBytes;
|
|
23892
|
+
otherMaxBytes;
|
|
23893
|
+
map = /* @__PURE__ */ new Map();
|
|
23894
|
+
/**
|
|
23895
|
+
* Bytes held PER PROFILE. Budgets are per profile and so is eviction: a
|
|
23896
|
+
* `high` entry must never be able to evict the `low` segments the drag is
|
|
23897
|
+
* running on, and the reverse would be just as wrong.
|
|
23898
|
+
*/
|
|
23899
|
+
bytesByProfile = /* @__PURE__ */ new Map();
|
|
23900
|
+
constructor(maxBytes = SCRUB_FRAGMENT_CACHE_MAX_BYTES, otherMaxBytes = SCRUB_FRAGMENT_CACHE_OTHER_MAX_BYTES) {
|
|
23901
|
+
this.maxBytes = maxBytes;
|
|
23902
|
+
this.otherMaxBytes = otherMaxBytes;
|
|
23903
|
+
}
|
|
23904
|
+
/** The byte budget for a profile — the scrub profile gets the real one. */
|
|
23905
|
+
budgetFor(profile) {
|
|
23906
|
+
return profile === "low" ? this.maxBytes : this.otherMaxBytes;
|
|
23907
|
+
}
|
|
23908
|
+
key(profile, startMs) {
|
|
23909
|
+
return `${profile}|${startMs}`;
|
|
23910
|
+
}
|
|
23911
|
+
/** Exact-start lookup with LRU touch (re-insertion marks most recent). */
|
|
23912
|
+
get(profile, startMs) {
|
|
23913
|
+
const k = this.key(profile, startMs);
|
|
23914
|
+
const frag = this.map.get(k);
|
|
23915
|
+
if (!frag) return null;
|
|
23916
|
+
this.map.delete(k);
|
|
23917
|
+
this.map.set(k, frag);
|
|
23918
|
+
return frag;
|
|
23919
|
+
}
|
|
23920
|
+
/**
|
|
23921
|
+
* The cached fragment COVERING an instant, if any — the lookup that lets a
|
|
23922
|
+
* warm drag tick skip `locate` entirely (an RPC measured at 1–8 s on a
|
|
23923
|
+
* saturated hub). Coverage is proof of footage: recorded fragments are
|
|
23924
|
+
* immutable, so a hit needs no re-validation. LRU-touches the hit.
|
|
23925
|
+
*/
|
|
23926
|
+
covering(profile, epochMs) {
|
|
23927
|
+
for (const [k, frag] of this.map) {
|
|
23928
|
+
if (!k.startsWith(`${profile}|`)) continue;
|
|
23929
|
+
if (epochMs >= frag.startMs && epochMs < frag.startMs + frag.durMs) {
|
|
23930
|
+
this.map.delete(k);
|
|
23931
|
+
this.map.set(k, frag);
|
|
23932
|
+
return frag;
|
|
23933
|
+
}
|
|
23934
|
+
}
|
|
23935
|
+
return null;
|
|
23936
|
+
}
|
|
23937
|
+
/** Insert (or refresh) a fragment, evicting that PROFILE's oldest past its bound. */
|
|
23938
|
+
put(profile, frag) {
|
|
23939
|
+
const k = this.key(profile, frag.startMs);
|
|
23940
|
+
const held = this.map.get(k);
|
|
23941
|
+
let bytes = this.bytesByProfile.get(profile) ?? 0;
|
|
23942
|
+
if (held) bytes -= fragmentBytes(held);
|
|
23943
|
+
this.map.delete(k);
|
|
23944
|
+
this.map.set(k, frag);
|
|
23945
|
+
bytes += fragmentBytes(frag);
|
|
23946
|
+
this.bytesByProfile.set(profile, bytes);
|
|
23947
|
+
const budget = this.budgetFor(profile);
|
|
23948
|
+
const prefix = `${profile}|`;
|
|
23949
|
+
while ((this.bytesByProfile.get(profile) ?? 0) > budget && this.entriesFor(profile) > 4) {
|
|
23950
|
+
const oldest = this.oldestKeyWithPrefix(prefix);
|
|
23951
|
+
if (oldest === void 0) break;
|
|
23952
|
+
const evicted = this.map.get(oldest);
|
|
23953
|
+
this.map.delete(oldest);
|
|
23954
|
+
if (evicted) this.bytesByProfile.set(profile, (this.bytesByProfile.get(profile) ?? 0) - fragmentBytes(evicted));
|
|
23955
|
+
}
|
|
23956
|
+
}
|
|
23957
|
+
oldestKeyWithPrefix(prefix) {
|
|
23958
|
+
for (const k of this.map.keys()) if (k.startsWith(prefix)) return k;
|
|
23959
|
+
}
|
|
23960
|
+
entriesFor(profile) {
|
|
23961
|
+
const prefix = `${profile}|`;
|
|
23962
|
+
let n = 0;
|
|
23963
|
+
for (const k of this.map.keys()) if (k.startsWith(prefix)) n += 1;
|
|
23964
|
+
return n;
|
|
23965
|
+
}
|
|
23966
|
+
/** Demuxed bytes currently held across every profile. */
|
|
23967
|
+
get byteSize() {
|
|
23968
|
+
let total = 0;
|
|
23969
|
+
for (const bytes of this.bytesByProfile.values()) total += bytes;
|
|
23970
|
+
return total;
|
|
23971
|
+
}
|
|
23972
|
+
/** Demuxed bytes held for one profile — the number its MB bound governs. */
|
|
23973
|
+
byteSizeFor(profile) {
|
|
23974
|
+
return this.bytesByProfile.get(profile) ?? 0;
|
|
23975
|
+
}
|
|
23976
|
+
get size() {
|
|
23977
|
+
return this.map.size;
|
|
23978
|
+
}
|
|
23979
|
+
};
|
|
23980
|
+
/**
|
|
23981
|
+
* `velocity` is ms of timeline per ms of wall clock, signed. `readMs` is what a
|
|
23982
|
+
* read costs on this disk right now. Either being null means "not enough is
|
|
23983
|
+
* known to aim" — which is NOT saturation: an unknown velocity must not switch
|
|
23984
|
+
* speculation off, or the first ticks of every drag would lose it.
|
|
23985
|
+
*/
|
|
23986
|
+
function scrubLeadDecision(velocity, readMs) {
|
|
23987
|
+
if (velocity === null || readMs === null) return {
|
|
23988
|
+
leadMs: 0,
|
|
23989
|
+
saturated: false
|
|
23990
|
+
};
|
|
23991
|
+
const raw = Math.round(velocity * readMs);
|
|
23992
|
+
if (Math.abs(raw) > 3e4) return {
|
|
23993
|
+
leadMs: 0,
|
|
23994
|
+
saturated: true
|
|
23995
|
+
};
|
|
23996
|
+
return {
|
|
23997
|
+
leadMs: raw,
|
|
23998
|
+
saturated: false
|
|
23999
|
+
};
|
|
24000
|
+
}
|
|
23961
24001
|
//#endregion
|
|
23962
24002
|
//#region src/stream-broker/recorded/recorded-feeder.ts
|
|
24003
|
+
/**
|
|
24004
|
+
* READ-AHEAD LEAD — the fix for "the picture chases the finger".
|
|
24005
|
+
*
|
|
24006
|
+
* A scrub frame is always the frame a read STARTED for, one read time ago.
|
|
24007
|
+
* On this disk that is 531 ms of wall clock (p50, census 2026-08-25), and on
|
|
24008
|
+
* a wide window 531 ms of gesture is TWENTY MINUTES of timeline: measured in
|
|
24009
|
+
* the drag bench, the median painted frame on a 2 h fling was 1 220 000 ms
|
|
24010
|
+
* behind the finger. Nothing about that is I/O — it is aiming at where the
|
|
24011
|
+
* finger WAS.
|
|
24012
|
+
*
|
|
24013
|
+
* So a steady drag reads where the finger WILL BE: `mediaVelocity ×
|
|
24014
|
+
* medianRecentReadMs`. It costs nothing (the same one read, at a different
|
|
24015
|
+
* offset), and it is bounded by the two things that make it a prediction
|
|
24016
|
+
* rather than a guess:
|
|
24017
|
+
*
|
|
24018
|
+
* - a direction RUN of at least {@link SCRUB_LEAD_MIN_DIR_RUN} POINTER
|
|
24019
|
+
* samples, so a jittery or reversing drag never leads. Pointer samples and
|
|
24020
|
+
* not pushes: on this disk a 3 s drag produces ~75 pointer samples and only
|
|
24021
|
+
* ~4 pushes, so a gate counted in pushes has no runway inside the gesture
|
|
24022
|
+
* it is supposed to help — measured in the drag bench, it engaged once, at
|
|
24023
|
+
* the very end;
|
|
24024
|
+
* - at least {@link SCRUB_LEAD_MIN_READ_SAMPLES} measured read, so the lead
|
|
24025
|
+
* is derived from this disk right now and not from a constant. ONE is
|
|
24026
|
+
* enough: against a read cost that is unknown, a one-sample estimate is not
|
|
24027
|
+
* a good prediction — it is simply a far better one than assuming zero.
|
|
24028
|
+
*
|
|
24029
|
+
* Overshoot is self-correcting: the retarget below serves the newest target
|
|
24030
|
+
* the landed fragment covers, so a lead that is slightly long lands on the
|
|
24031
|
+
* finger exactly, and one that is wrong costs the same single read it would
|
|
24032
|
+
* have cost anyway.
|
|
24033
|
+
*/
|
|
23963
24034
|
var SEGMENT_TIMEOUT_MS = 8e3;
|
|
23964
24035
|
/**
|
|
23965
24036
|
* Deadline for the recorded-AUDIO demux on the `reusedWhole` seek path, and
|
|
@@ -24060,30 +24131,6 @@ var SEEK_ANIMATION_MIN_DISTANCE_MS = 8e3;
|
|
|
24060
24131
|
*/
|
|
24061
24132
|
var SCRUB_JUMP_TRAVEL_MAX_STILLS = 3;
|
|
24062
24133
|
/**
|
|
24063
|
-
* Consecutive same-direction `scrubTo` deltas required before a direction-aware
|
|
24064
|
-
* prefetch commits its (single) background slot. Two deltas ⇒ the drag shows a
|
|
24065
|
-
* sustained direction, so a jittery/reversing drag never wastes the slot.
|
|
24066
|
-
*/
|
|
24067
|
-
var SCRUB_PREFETCH_DIR_THRESHOLD = 2;
|
|
24068
|
-
/**
|
|
24069
|
-
* How many speculative GOP reads may be in flight at once, i.e. how many drag
|
|
24070
|
-
* steps ahead the feeder reads.
|
|
24071
|
-
*
|
|
24072
|
-
* The scrub tick IS the read: measured on salone 590 over `low`,
|
|
24073
|
-
* `tickMsP50 = 270` of which `readMsP50 = 263` — a cold ~135 KB fragment off
|
|
24074
|
-
* the HDD array through shfs/FUSE, `demuxMsP50 = 1`. One read at a time is a
|
|
24075
|
-
* hard ~3.8 push/s ceiling no cache can lift, because a drag whose steps are
|
|
24076
|
-
* tens of seconds apart revisits nothing. Reading N steps ahead is the only
|
|
24077
|
-
* thing that converts wall-clock the drag is ALREADY spending into pushes.
|
|
24078
|
-
*
|
|
24079
|
-
* 2 and not more: these reads land on the same spindles the recorder is writing
|
|
24080
|
-
* every camera to, and past a small number concurrent random reads stop
|
|
24081
|
-
* overlapping and start thrashing. Raise it only against a measured
|
|
24082
|
-
* `prefetchHits` / `readMsP50` pair — it is the knob, and the census carries
|
|
24083
|
-
* both halves of the evidence.
|
|
24084
|
-
*/
|
|
24085
|
-
var SCRUB_PREFETCH_MAX_INFLIGHT = 2;
|
|
24086
|
-
/**
|
|
24087
24134
|
* The profile whose scrub read unit is the WHOLE SEGMENT.
|
|
24088
24135
|
*
|
|
24089
24136
|
* Every drag is pinned to `low` by the client, and a `low` segment is ~230 KB
|
|
@@ -24119,14 +24166,30 @@ var SCRUB_WHOLE_SEGMENT_PROFILE = "low";
|
|
|
24119
24166
|
* (`scrub-feeder:oversize-segment`, `oversizeSegments` on the drag census)
|
|
24120
24167
|
* instead of being inferred from a falling hit rate.
|
|
24121
24168
|
*
|
|
24122
|
-
* The real fix, when 60 s segments land, is a read unit that is a BYTE
|
|
24123
|
-
* over the fragment's own keyframe table — "as many consecutive GOPs
|
|
24124
|
-
* N bytes" —
|
|
24125
|
-
* (`
|
|
24126
|
-
* on
|
|
24169
|
+
* The real fix, for when 60 s segments land, is a read unit that is a BYTE
|
|
24170
|
+
* BUDGET over the fragment's own keyframe table — "as many consecutive GOPs
|
|
24171
|
+
* as fit in N bytes" — computed from `tfra` offsets alone, no payload read
|
|
24172
|
+
* (`fragmentsWithinBudget`, `recorder/mfra-index.ts`). `readGopBytes` now uses
|
|
24173
|
+
* exactly this budget on the fallback path below: it no longer reads exactly
|
|
24174
|
+
* one GOP, it reads as many CONSECUTIVE fragments as fit this same ceiling, in
|
|
24175
|
+
* one range read (`gop-range-read.ts`'s `maxBytes`). The recorder-side handler
|
|
24176
|
+
* (`recording-provider.ts`) declares its OWN copy of this number
|
|
24177
|
+
* (`SCRUB_GOP_BUDGET_MAX_BYTES` in `gop-range-read.ts`) rather than importing
|
|
24178
|
+
* this module — the client feeder is a different addon-internal concern, and
|
|
24179
|
+
* pulling its whole dependency graph in for one number is the wrong
|
|
24180
|
+
* direction. Exported here so a future caller in THIS process can reuse it;
|
|
24181
|
+
* if D163's number moves, move both.
|
|
24127
24182
|
*/
|
|
24128
24183
|
var SCRUB_WHOLE_SEGMENT_MAX_BYTES = 15e5;
|
|
24129
24184
|
/**
|
|
24185
|
+
* How still the finger must be before the window warm may take the disk back.
|
|
24186
|
+
*
|
|
24187
|
+
* Long enough that it does not thrash on the gap between two ticks of a slow
|
|
24188
|
+
* drag (tickMsP50 was 120-129 ms on the measured drags), short enough that a
|
|
24189
|
+
* pause the operator would call "stopped" is warm again before they move.
|
|
24190
|
+
*/
|
|
24191
|
+
var SCRUB_WARM_QUIET_MS = 300;
|
|
24192
|
+
/**
|
|
24130
24193
|
* How far the entry warm-up reaches, backward and forward from the playhead the
|
|
24131
24194
|
* drag starts on.
|
|
24132
24195
|
*
|
|
@@ -24161,17 +24224,6 @@ var SCRUB_WARM_MAX_SEGMENTS = Math.max(1, Math.floor(SCRUB_FRAGMENT_CACHE_MAX_BY
|
|
|
24161
24224
|
*/
|
|
24162
24225
|
var IDLE_WARM_READ_GAP_MS = 250;
|
|
24163
24226
|
/**
|
|
24164
|
-
* Median recent read time at or above which the feeder STOPS speculating.
|
|
24165
|
-
*
|
|
24166
|
-
* Speculation is a bet that spare I/O exists. When reads are this slow there is
|
|
24167
|
-
* no spare I/O — the array is saturated — and a prefetch is not free work, it
|
|
24168
|
-
* is another random seek queued ahead of the read the operator is waiting on.
|
|
24169
|
-
* The field regime that forced this: 54 prefetch reads for 9 hits with the disk
|
|
24170
|
-
* at 98% busy. Suppression is counted (`prefetchSuppressed`) so the decision is
|
|
24171
|
-
* visible, and it lifts by itself as soon as reads come back.
|
|
24172
|
-
*/
|
|
24173
|
-
var SCRUB_SLOW_READ_MS = 600;
|
|
24174
|
-
/**
|
|
24175
24227
|
* Max forward TARGET delta (ms of timeline, between consecutive drag targets)
|
|
24176
24228
|
* that SWEEPS predicted frames instead of jumping. A steady forward drag is
|
|
24177
24229
|
* playback with a moving target: once a keyframe decoded, the predicted frames
|
|
@@ -24244,37 +24296,6 @@ var SCRUB_RIDE_WINDOW_MS = 1e4;
|
|
|
24244
24296
|
* moved on to. At the cap, a tick rides the newest in-flight read instead.
|
|
24245
24297
|
*/
|
|
24246
24298
|
var SCRUB_RESOLVE_MAX_INFLIGHT = 2;
|
|
24247
|
-
/**
|
|
24248
|
-
* READ-AHEAD LEAD — the fix for "the picture chases the finger".
|
|
24249
|
-
*
|
|
24250
|
-
* A scrub frame is always the frame a read STARTED for, one read time ago.
|
|
24251
|
-
* On this disk that is 531 ms of wall clock (p50, census 2026-08-25), and on
|
|
24252
|
-
* a wide window 531 ms of gesture is TWENTY MINUTES of timeline: measured in
|
|
24253
|
-
* the drag bench, the median painted frame on a 2 h fling was 1 220 000 ms
|
|
24254
|
-
* behind the finger. Nothing about that is I/O — it is aiming at where the
|
|
24255
|
-
* finger WAS.
|
|
24256
|
-
*
|
|
24257
|
-
* So a steady drag reads where the finger WILL BE: `mediaVelocity ×
|
|
24258
|
-
* medianRecentReadMs`. It costs nothing (the same one read, at a different
|
|
24259
|
-
* offset), and it is bounded by the two things that make it a prediction
|
|
24260
|
-
* rather than a guess:
|
|
24261
|
-
*
|
|
24262
|
-
* - a direction RUN of at least {@link SCRUB_LEAD_MIN_DIR_RUN} POINTER
|
|
24263
|
-
* samples, so a jittery or reversing drag never leads. Pointer samples and
|
|
24264
|
-
* not pushes: on this disk a 3 s drag produces ~75 pointer samples and only
|
|
24265
|
-
* ~4 pushes, so a gate counted in pushes has no runway inside the gesture
|
|
24266
|
-
* it is supposed to help — measured in the drag bench, it engaged once, at
|
|
24267
|
-
* the very end;
|
|
24268
|
-
* - at least {@link SCRUB_LEAD_MIN_READ_SAMPLES} measured read, so the lead
|
|
24269
|
-
* is derived from this disk right now and not from a constant. ONE is
|
|
24270
|
-
* enough: against a read cost that is unknown, a one-sample estimate is not
|
|
24271
|
-
* a good prediction — it is simply a far better one than assuming zero.
|
|
24272
|
-
*
|
|
24273
|
-
* Overshoot is self-correcting: the retarget below serves the newest target
|
|
24274
|
-
* the landed fragment covers, so a lead that is slightly long lands on the
|
|
24275
|
-
* finger exactly, and one that is wrong costs the same single read it would
|
|
24276
|
-
* have cost anyway.
|
|
24277
|
-
*/
|
|
24278
24299
|
var SCRUB_LEAD_MIN_READ_SAMPLES = 1;
|
|
24279
24300
|
/**
|
|
24280
24301
|
* Smoothing for the media-velocity estimate (ms of timeline per ms of wall
|
|
@@ -24507,25 +24528,6 @@ var RecordedFeeder = class {
|
|
|
24507
24528
|
scrubLastPushLogAtMs = 0;
|
|
24508
24529
|
scrubLastSlowLocateLogAtMs = 0;
|
|
24509
24530
|
/**
|
|
24510
|
-
* Signed run-length of consecutive same-direction `scrubTo` deltas (+ forward,
|
|
24511
|
-
* − backward). A prefetch commits only once `|run| ≥ SCRUB_PREFETCH_DIR_THRESHOLD`;
|
|
24512
|
-
* a reversal or an in-fragment no-move resets it.
|
|
24513
|
-
*/
|
|
24514
|
-
scrubDirRun = 0;
|
|
24515
|
-
/**
|
|
24516
|
-
* Prefetch reads in flight. Bounded by {@link SCRUB_PREFETCH_MAX_INFLIGHT}:
|
|
24517
|
-
* these are the SPECULATIVE concurrency slots, and they only fill
|
|
24518
|
-
* `scrubCache` — they never push, never touch the pending/timer/latest-wins
|
|
24519
|
-
* state, and never block the live coalesced tick.
|
|
24520
|
-
*
|
|
24521
|
-
* More than one because the read IS the tick (263 of 270 ms measured): with a
|
|
24522
|
-
* single slot the drag can never go faster than one cold read, however well
|
|
24523
|
-
* the speculation guesses.
|
|
24524
|
-
*/
|
|
24525
|
-
scrubPrefetchInFlight = 0;
|
|
24526
|
-
/** Probe instants currently being read, so two horizons never duplicate one. */
|
|
24527
|
-
scrubPrefetchProbes = /* @__PURE__ */ new Set();
|
|
24528
|
-
/**
|
|
24529
24531
|
* Fragment starts this drag put in the cache SPECULATIVELY. A later tick that
|
|
24530
24532
|
* hits one of them is what makes speculation provably worth its I/O
|
|
24531
24533
|
* (`prefetchHits`) — without this the feeder could double the load on the disk
|
|
@@ -24575,6 +24577,18 @@ var RecordedFeeder = class {
|
|
|
24575
24577
|
scrubFillLoopActive = false;
|
|
24576
24578
|
scrubWarmBackMs = SCRUB_WARM_BACK_MS;
|
|
24577
24579
|
scrubWarmFwdMs = SCRUB_WARM_FWD_MS;
|
|
24580
|
+
/**
|
|
24581
|
+
* The client-supplied id of the CURRENT (or most recently active) scrub
|
|
24582
|
+
* gesture, set by `beginScrub(dragId)`. Printed on every `scrub-feeder:*`
|
|
24583
|
+
* line — ticks, warm fill, park/deadline/ack pathologies, and the drag-end
|
|
24584
|
+
* aggregate — so a drag can be isolated in the logs without depending on
|
|
24585
|
+
* wall-clock proximity. `undefined` outside a drag (and for a feeder whose
|
|
24586
|
+
* caller never sends one — additive, optional, see D1 in the plan).
|
|
24587
|
+
*/
|
|
24588
|
+
scrubDragId;
|
|
24589
|
+
/** This feeder's own last-announced state — the `from` half of every
|
|
24590
|
+
* {@link FeederStateTransition}. `null` before the first transition. */
|
|
24591
|
+
lastAnnouncedState = null;
|
|
24578
24592
|
constructor(deps) {
|
|
24579
24593
|
this.deps = deps;
|
|
24580
24594
|
this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3);
|
|
@@ -24592,6 +24606,50 @@ var RecordedFeeder = class {
|
|
|
24592
24606
|
if (typeof pcmu === "function") return (data, rtpTs) => pcmu.call(sink, data, rtpTs);
|
|
24593
24607
|
}
|
|
24594
24608
|
/**
|
|
24609
|
+
* Emit one state transition through `onState` — see
|
|
24610
|
+
* {@link FeederStateTransition} for why `from`/`reason`/`seekToken` exist.
|
|
24611
|
+
* The ONLY place `this.lastAnnouncedState` is written, so every announced
|
|
24612
|
+
* state (wherever it is announced from) is captured as the next
|
|
24613
|
+
* transition's `from`.
|
|
24614
|
+
*/
|
|
24615
|
+
emitState(to, reason, seekToken, dragId) {
|
|
24616
|
+
const from = this.lastAnnouncedState;
|
|
24617
|
+
this.lastAnnouncedState = to;
|
|
24618
|
+
this.deps.onState({
|
|
24619
|
+
from,
|
|
24620
|
+
to,
|
|
24621
|
+
reason,
|
|
24622
|
+
seekToken,
|
|
24623
|
+
dragId
|
|
24624
|
+
});
|
|
24625
|
+
}
|
|
24626
|
+
/**
|
|
24627
|
+
* ONE line per `seek()` call, unconditionally — the C3 rendiconto. Before
|
|
24628
|
+
* this, a commit's outcome was told by WHICHEVER of several lines happened
|
|
24629
|
+
* to fire: `recorded:seek-ready` only on success, `recorded:seek-abandoned`
|
|
24630
|
+
* only when a later token/dispose superseded this one, `recorded:seek-no-footage`
|
|
24631
|
+
* / `recorded:snap-too-far` only on a gap — and a raw thrown error (or an
|
|
24632
|
+
* error on an ALREADY-abandoned commit) logged NOTHING at all. That silent
|
|
24633
|
+
* shape is exactly the 2026-08-26 stall: two commits reached
|
|
24634
|
+
* `playback:intent` and never `playback:ready`, with nothing in the feeder's
|
|
24635
|
+
* own logs saying why. Every return path in `seek()` now calls this before
|
|
24636
|
+
* it exits, so a commit that never lands still leaves one line naming what
|
|
24637
|
+
* happened to it.
|
|
24638
|
+
*/
|
|
24639
|
+
logSeekOutcome(token, seekStartedAt, epochMs, dragId, outcome, state) {
|
|
24640
|
+
this.deps.onLog?.("recorded:seek-outcome", {
|
|
24641
|
+
dragId,
|
|
24642
|
+
epochMs,
|
|
24643
|
+
outcome,
|
|
24644
|
+
state,
|
|
24645
|
+
disposed: this.disposed,
|
|
24646
|
+
superseded: token !== this.seekToken,
|
|
24647
|
+
tokenAtStart: token,
|
|
24648
|
+
tokenNow: this.seekToken,
|
|
24649
|
+
totalMs: Math.round(performance.now() - seekStartedAt)
|
|
24650
|
+
});
|
|
24651
|
+
}
|
|
24652
|
+
/**
|
|
24595
24653
|
* Seek to an epoch and (if rate > 0) begin forward playback. Resolves after
|
|
24596
24654
|
* the FIRST located segment is fed; the forward `advance` loop is started
|
|
24597
24655
|
* fire-and-forget so seek never blocks on its pacing. Latest-wins: a newer
|
|
@@ -24604,6 +24662,7 @@ var RecordedFeeder = class {
|
|
|
24604
24662
|
let loadMs = 0;
|
|
24605
24663
|
let firstFrameMs = 0;
|
|
24606
24664
|
const token = ++this.seekToken;
|
|
24665
|
+
const dragId = opts?.dragId;
|
|
24607
24666
|
const wasScrubbing = this.scrubbing;
|
|
24608
24667
|
if (this.scrubbing) {
|
|
24609
24668
|
this.emitDragEnd();
|
|
@@ -24615,7 +24674,6 @@ var RecordedFeeder = class {
|
|
|
24615
24674
|
this.clearScrubAckTimeout();
|
|
24616
24675
|
this.scrubPendingHint = null;
|
|
24617
24676
|
this.scrubPrevTarget = null;
|
|
24618
|
-
this.scrubDirRun = 0;
|
|
24619
24677
|
if (this.scrubTimer) {
|
|
24620
24678
|
clearTimeout(this.scrubTimer);
|
|
24621
24679
|
this.scrubTimer = null;
|
|
@@ -24625,7 +24683,7 @@ var RecordedFeeder = class {
|
|
|
24625
24683
|
this.inFlightSeekEpochMs = epochMs;
|
|
24626
24684
|
let snapInfo = null;
|
|
24627
24685
|
try {
|
|
24628
|
-
this.
|
|
24686
|
+
this.emitState("loading", "seek-start", token, dragId);
|
|
24629
24687
|
if (this.clockHasAdvanced) {
|
|
24630
24688
|
this.clock.markDiscontinuity(SEEK_DISCONTINUITY_MS);
|
|
24631
24689
|
this.audioClock.markDiscontinuity(SEEK_DISCONTINUITY_MS);
|
|
@@ -24637,21 +24695,29 @@ var RecordedFeeder = class {
|
|
|
24637
24695
|
epochMs
|
|
24638
24696
|
}));
|
|
24639
24697
|
locateMs += performance.now() - locateStartedAt;
|
|
24640
|
-
if (this.disposed || token !== this.seekToken)
|
|
24698
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24699
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24700
|
+
return;
|
|
24701
|
+
}
|
|
24641
24702
|
let landMs = epochMs;
|
|
24642
24703
|
if (loc.kind === "gap") {
|
|
24643
24704
|
const decision = resolveSnapTarget(loc, epochMs);
|
|
24644
24705
|
if (decision.kind !== "snap") {
|
|
24645
24706
|
if (decision.kind === "too-far") this.deps.onLog?.("recorded:snap-too-far", {
|
|
24707
|
+
dragId,
|
|
24646
24708
|
requestedMs: epochMs,
|
|
24647
24709
|
nearestMs: decision.snapMs,
|
|
24648
24710
|
distanceMs: decision.distanceMs
|
|
24649
24711
|
});
|
|
24650
|
-
else this.deps.onLog?.("recorded:seek-no-footage", {
|
|
24712
|
+
else this.deps.onLog?.("recorded:seek-no-footage", {
|
|
24713
|
+
dragId,
|
|
24714
|
+
requestedMs: epochMs
|
|
24715
|
+
});
|
|
24651
24716
|
this.currentSegment = null;
|
|
24652
24717
|
this.lastFedVideoIdx = -1;
|
|
24653
24718
|
this.stopPositionReports();
|
|
24654
|
-
this.
|
|
24719
|
+
this.emitState("gap", decision.kind === "too-far" ? "seek-snap-too-far" : "seek-no-footage", token, dragId);
|
|
24720
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "gap", "gap");
|
|
24655
24721
|
return;
|
|
24656
24722
|
}
|
|
24657
24723
|
const snapLocateStartedAt = performance.now();
|
|
@@ -24661,19 +24727,25 @@ var RecordedFeeder = class {
|
|
|
24661
24727
|
epochMs: decision.snapMs
|
|
24662
24728
|
}));
|
|
24663
24729
|
locateMs += performance.now() - snapLocateStartedAt;
|
|
24664
|
-
if (this.disposed || token !== this.seekToken)
|
|
24730
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24731
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24732
|
+
return;
|
|
24733
|
+
}
|
|
24665
24734
|
if (snapped.kind !== "segment") {
|
|
24666
24735
|
this.deps.onLog?.("recorded:seek-no-footage", {
|
|
24736
|
+
dragId,
|
|
24667
24737
|
requestedMs: epochMs,
|
|
24668
24738
|
snappedMs: decision.snapMs
|
|
24669
24739
|
});
|
|
24670
24740
|
this.currentSegment = null;
|
|
24671
24741
|
this.lastFedVideoIdx = -1;
|
|
24672
24742
|
this.stopPositionReports();
|
|
24673
|
-
this.
|
|
24743
|
+
this.emitState("gap", "seek-snap-target-no-footage", token, dragId);
|
|
24744
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "gap", "gap");
|
|
24674
24745
|
return;
|
|
24675
24746
|
}
|
|
24676
24747
|
this.deps.onLog?.("recorded:seek-snapped", {
|
|
24748
|
+
dragId,
|
|
24677
24749
|
requestedMs: epochMs,
|
|
24678
24750
|
snappedMs: decision.snapMs
|
|
24679
24751
|
});
|
|
@@ -24687,9 +24759,12 @@ var RecordedFeeder = class {
|
|
|
24687
24759
|
const travelFromMs = opts?.travelFromMs ?? (this.clockHasAdvanced ? this.cursorMs : null);
|
|
24688
24760
|
if (!wasScrubbing && travelFromMs !== null) {
|
|
24689
24761
|
const travelStartedAt = performance.now();
|
|
24690
|
-
await this.animateSeekTravel(travelFromMs, landMs, token);
|
|
24762
|
+
await this.animateSeekTravel(travelFromMs, landMs, token, dragId);
|
|
24691
24763
|
travelMs = performance.now() - travelStartedAt;
|
|
24692
|
-
if (this.disposed || token !== this.seekToken)
|
|
24764
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24765
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24766
|
+
return;
|
|
24767
|
+
}
|
|
24693
24768
|
}
|
|
24694
24769
|
this.cursorMs = loc.startMs;
|
|
24695
24770
|
const loadStartedAt = performance.now();
|
|
@@ -24706,7 +24781,10 @@ var RecordedFeeder = class {
|
|
|
24706
24781
|
prefixSource = "cache";
|
|
24707
24782
|
} else {
|
|
24708
24783
|
const fragmentLoad = await this.loadScrubFragment(loc, landMs);
|
|
24709
|
-
if (fragmentLoad === null)
|
|
24784
|
+
if (fragmentLoad === null) {
|
|
24785
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24786
|
+
return;
|
|
24787
|
+
}
|
|
24710
24788
|
fragment = fragmentLoad.seg;
|
|
24711
24789
|
this.scrubCache.put(this.deps.profile, fragment);
|
|
24712
24790
|
prefixSource = fragmentLoad.wholeSegmentRead ? "whole" : "gop";
|
|
@@ -24715,7 +24793,10 @@ var RecordedFeeder = class {
|
|
|
24715
24793
|
bytes: fragmentLoad.rawBytes
|
|
24716
24794
|
};
|
|
24717
24795
|
}
|
|
24718
|
-
if (this.disposed || token !== this.seekToken)
|
|
24796
|
+
if (this.disposed || token !== this.seekToken) {
|
|
24797
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24798
|
+
return;
|
|
24799
|
+
}
|
|
24719
24800
|
const fragmentLoc = {
|
|
24720
24801
|
kind: "segment",
|
|
24721
24802
|
startMs: fragment.startMs,
|
|
@@ -24726,7 +24807,7 @@ var RecordedFeeder = class {
|
|
|
24726
24807
|
prefixFed = true;
|
|
24727
24808
|
gopFirst = true;
|
|
24728
24809
|
firstFrameMs = performance.now() - seekStartedAt;
|
|
24729
|
-
this.
|
|
24810
|
+
this.emitState(this.rate === 0 ? "paused" : "playing", "seek-gop-prefix-landed", token, dragId);
|
|
24730
24811
|
this.deps.onPosition(this.cursorMs);
|
|
24731
24812
|
this.startPositionReports();
|
|
24732
24813
|
stateAnnounced = true;
|
|
@@ -24741,6 +24822,7 @@ var RecordedFeeder = class {
|
|
|
24741
24822
|
audio = [...await this.withTimeout(this.deps.demuxAudio(reusedWhole.bytes), AUDIO_DEMUX_TIMEOUT_MS)];
|
|
24742
24823
|
} catch (err) {
|
|
24743
24824
|
this.deps.onLog?.("recorded:seek-audio-dropped", {
|
|
24825
|
+
dragId,
|
|
24744
24826
|
waitedMs: Date.now() - audioStartedAt,
|
|
24745
24827
|
budgetMs: AUDIO_DEMUX_TIMEOUT_MS,
|
|
24746
24828
|
bytes: reusedWhole.bytes.byteLength,
|
|
@@ -24768,7 +24850,21 @@ var RecordedFeeder = class {
|
|
|
24768
24850
|
loadAusMs = performance.now() - loadAusStartedAt;
|
|
24769
24851
|
}
|
|
24770
24852
|
loadMs = performance.now() - loadStartedAt;
|
|
24771
|
-
if (this.disposed || token !== this.seekToken || loaded === null)
|
|
24853
|
+
if (this.disposed || token !== this.seekToken || loaded === null) {
|
|
24854
|
+
this.deps.onLog?.("recorded:seek-abandoned", {
|
|
24855
|
+
dragId,
|
|
24856
|
+
requestedMs: epochMs,
|
|
24857
|
+
disposed: this.disposed,
|
|
24858
|
+
superseded: token !== this.seekToken,
|
|
24859
|
+
tokenAtStart: token,
|
|
24860
|
+
tokenNow: this.seekToken,
|
|
24861
|
+
loadedNull: loaded === null,
|
|
24862
|
+
announcedLoading: true,
|
|
24863
|
+
loadMs: Math.round(loadMs)
|
|
24864
|
+
});
|
|
24865
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24866
|
+
return;
|
|
24867
|
+
}
|
|
24772
24868
|
if (!prefixFed) this.feedPrefix(loc, loaded.video, landMs);
|
|
24773
24869
|
if (gopFirst) {
|
|
24774
24870
|
this.currentSegment = {
|
|
@@ -24790,11 +24886,12 @@ var RecordedFeeder = class {
|
|
|
24790
24886
|
}
|
|
24791
24887
|
if (this.rate === 0) {
|
|
24792
24888
|
if (!stateAnnounced) {
|
|
24793
|
-
this.
|
|
24889
|
+
this.emitState("paused", "seek-landed-paused", token, dragId);
|
|
24794
24890
|
this.deps.onPosition(this.cursorMs);
|
|
24795
24891
|
this.startPositionReports();
|
|
24796
24892
|
}
|
|
24797
24893
|
this.logSeekReady({
|
|
24894
|
+
dragId,
|
|
24798
24895
|
epochMs,
|
|
24799
24896
|
startedAt: seekStartedAt,
|
|
24800
24897
|
locateMs,
|
|
@@ -24810,14 +24907,16 @@ var RecordedFeeder = class {
|
|
|
24810
24907
|
loadAusMs: Math.round(loadAusMs)
|
|
24811
24908
|
});
|
|
24812
24909
|
this.startIdleWarm(this.cursorMs);
|
|
24910
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "served", "paused");
|
|
24813
24911
|
return;
|
|
24814
24912
|
}
|
|
24815
24913
|
if (!stateAnnounced) {
|
|
24816
|
-
this.
|
|
24914
|
+
this.emitState("playing", "seek-landed-playing", token, dragId);
|
|
24817
24915
|
this.deps.onPosition(this.cursorMs);
|
|
24818
24916
|
this.startPositionReports();
|
|
24819
24917
|
}
|
|
24820
24918
|
this.logSeekReady({
|
|
24919
|
+
dragId,
|
|
24821
24920
|
epochMs,
|
|
24822
24921
|
startedAt: seekStartedAt,
|
|
24823
24922
|
locateMs,
|
|
@@ -24847,22 +24946,26 @@ var RecordedFeeder = class {
|
|
|
24847
24946
|
resumeAfterPtsMs
|
|
24848
24947
|
});
|
|
24849
24948
|
this.startIdleWarm(this.cursorMs);
|
|
24949
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "served", "playing");
|
|
24850
24950
|
} catch (err) {
|
|
24851
24951
|
if (!this.disposed && token === this.seekToken) {
|
|
24852
24952
|
this.stopPositionReports();
|
|
24853
24953
|
if (snapInfo !== null) {
|
|
24854
24954
|
this.deps.onLog?.("recorded:seek-snap-load-failed", {
|
|
24955
|
+
dragId,
|
|
24855
24956
|
requestedMs: snapInfo.requestedMs,
|
|
24856
24957
|
snappedMs: snapInfo.snappedMs,
|
|
24857
24958
|
error: errMsg(err)
|
|
24858
24959
|
});
|
|
24859
24960
|
this.currentSegment = null;
|
|
24860
24961
|
this.lastFedVideoIdx = -1;
|
|
24861
|
-
this.
|
|
24962
|
+
this.emitState("gap", "seek-snap-load-failed", token, dragId);
|
|
24963
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "gap", "gap");
|
|
24862
24964
|
return;
|
|
24863
24965
|
}
|
|
24864
|
-
this.
|
|
24865
|
-
|
|
24966
|
+
this.emitState("error", "seek-error", token, dragId);
|
|
24967
|
+
this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "error", "error");
|
|
24968
|
+
} else this.logSeekOutcome(token, seekStartedAt, epochMs, dragId, "abandoned", null);
|
|
24866
24969
|
} finally {
|
|
24867
24970
|
if (token === this.seekToken) {
|
|
24868
24971
|
this.inFlightSeekEpochMs = null;
|
|
@@ -24872,6 +24975,7 @@ var RecordedFeeder = class {
|
|
|
24872
24975
|
}
|
|
24873
24976
|
logSeekReady(fields) {
|
|
24874
24977
|
this.deps.onLog?.("recorded:seek-ready", {
|
|
24978
|
+
dragId: fields.dragId,
|
|
24875
24979
|
epochMs: fields.epochMs,
|
|
24876
24980
|
state: fields.state,
|
|
24877
24981
|
totalMs: Math.round(performance.now() - fields.startedAt),
|
|
@@ -24906,7 +25010,7 @@ var RecordedFeeder = class {
|
|
|
24906
25010
|
if (next === 0) {
|
|
24907
25011
|
this.advanceToken++;
|
|
24908
25012
|
this.stopPositionReports();
|
|
24909
|
-
this.
|
|
25013
|
+
this.emitState("paused", "rate-zero", this.seekToken, this.scrubDragId);
|
|
24910
25014
|
return;
|
|
24911
25015
|
}
|
|
24912
25016
|
this.pendingStepDirs.length = 0;
|
|
@@ -24928,9 +25032,16 @@ var RecordedFeeder = class {
|
|
|
24928
25032
|
* (which flips `scrubbing` false and clears the session cache) or `dispose()`;
|
|
24929
25033
|
* a genuinely NEW drag is therefore always preceded by that exit, so its
|
|
24930
25034
|
* `beginScrub` runs the full reset below. (D-scrub-ux)
|
|
25035
|
+
*
|
|
25036
|
+
* `dragId` is the client's id for this gesture (optional — the viewer's main
|
|
25037
|
+
* session generates it; a caller that omits it gets identical behaviour,
|
|
25038
|
+
* just no id on the logs). Stored for the whole drag, including its commit:
|
|
25039
|
+
* every `scrub-feeder:*` line printed until the next `beginScrub` carries it
|
|
25040
|
+
* (C1).
|
|
24931
25041
|
*/
|
|
24932
|
-
beginScrub() {
|
|
25042
|
+
beginScrub(dragId) {
|
|
24933
25043
|
if (this.disposed || this.scrubbing) return;
|
|
25044
|
+
this.scrubDragId = dragId;
|
|
24934
25045
|
this.seekToken++;
|
|
24935
25046
|
this.advanceToken++;
|
|
24936
25047
|
this.scrubbing = true;
|
|
@@ -24939,7 +25050,6 @@ var RecordedFeeder = class {
|
|
|
24939
25050
|
this.scrubSegment = null;
|
|
24940
25051
|
this.scrubCredit = SCRUB_CREDIT_CAP;
|
|
24941
25052
|
this.scrubPrevTarget = null;
|
|
24942
|
-
this.scrubDirRun = 0;
|
|
24943
25053
|
this.scrubPrefetched = /* @__PURE__ */ new Set();
|
|
24944
25054
|
this.scrubCursor = null;
|
|
24945
25055
|
this.scrubNoFootageLogged = false;
|
|
@@ -25022,6 +25132,7 @@ var RecordedFeeder = class {
|
|
|
25022
25132
|
this.scrubWarmBackMs = opts?.backMs ?? SCRUB_WARM_BACK_MS;
|
|
25023
25133
|
this.scrubWarmFwdMs = opts?.fwdMs ?? SCRUB_WARM_FWD_MS;
|
|
25024
25134
|
this.deps.onLog?.("scrub-feeder:warm-start", {
|
|
25135
|
+
dragId: this.scrubDragId,
|
|
25025
25136
|
centreMs,
|
|
25026
25137
|
fillGen: this.scrubWarmGen
|
|
25027
25138
|
});
|
|
@@ -25047,6 +25158,7 @@ var RecordedFeeder = class {
|
|
|
25047
25158
|
lastWorked = gen;
|
|
25048
25159
|
if (gen === this.scrubWarmGen) {
|
|
25049
25160
|
this.deps.onLog?.("scrub-feeder:warm-done", {
|
|
25161
|
+
dragId: this.scrubDragId,
|
|
25050
25162
|
fillGen: gen,
|
|
25051
25163
|
idle: this.idleWarm && !this.scrubbing,
|
|
25052
25164
|
fetched,
|
|
@@ -25083,6 +25195,19 @@ var RecordedFeeder = class {
|
|
|
25083
25195
|
if (Math.abs(targetMs - centre) <= SCRUB_RECENTER_MS) return;
|
|
25084
25196
|
this.recenterScrubWarm(targetMs);
|
|
25085
25197
|
}
|
|
25198
|
+
/**
|
|
25199
|
+
* Is the operator's finger actually moving right now?
|
|
25200
|
+
*
|
|
25201
|
+
* `scrubbing` alone is not the question — a drag stays open while the finger
|
|
25202
|
+
* rests, and those pauses are exactly when the warm should work. The signal
|
|
25203
|
+
* is the pointer sample clock, which `noteScrubSample` stamps on every
|
|
25204
|
+
* `scrubTo` before any velocity gating, so it is true even for a drag too
|
|
25205
|
+
* slow or too jittery for the velocity estimator.
|
|
25206
|
+
*/
|
|
25207
|
+
fingerIsMoving() {
|
|
25208
|
+
if (!this.scrubbing) return false;
|
|
25209
|
+
return performance.now() - this.scrubLastSampleAtMs < SCRUB_WARM_QUIET_MS;
|
|
25210
|
+
}
|
|
25086
25211
|
async warmScrubWindow(centreMs, dragGen, fillGen, backMs, fwdMs) {
|
|
25087
25212
|
const spent = await this.warmScrubDirection(centreMs, -1, backMs, 0, dragGen, fillGen);
|
|
25088
25213
|
return this.warmScrubDirection(centreMs, 1, fwdMs, spent, dragGen, fillGen);
|
|
@@ -25122,6 +25247,10 @@ var RecordedFeeder = class {
|
|
|
25122
25247
|
durMs: warm.durMs,
|
|
25123
25248
|
fetched: false
|
|
25124
25249
|
};
|
|
25250
|
+
while (this.fingerIsMoving()) {
|
|
25251
|
+
if (!this.warmLoopAlive(dragGen) || fillGen !== this.scrubWarmGen) return null;
|
|
25252
|
+
await new Promise((r) => setTimeout(r, SCRUB_WARM_QUIET_MS));
|
|
25253
|
+
}
|
|
25125
25254
|
if (this.idleWarm && !this.scrubbing) {
|
|
25126
25255
|
await new Promise((r) => setTimeout(r, this.deps.idleWarmGapMs ?? IDLE_WARM_READ_GAP_MS));
|
|
25127
25256
|
while (this.inFlightSeekEpochMs !== null) {
|
|
@@ -25141,7 +25270,7 @@ var RecordedFeeder = class {
|
|
|
25141
25270
|
if (loc.kind !== "segment") return null;
|
|
25142
25271
|
if (!await this.waitForTickReadIdle(fillGen)) return null;
|
|
25143
25272
|
const load = await this.loadScrubFragment(loc, probeMs);
|
|
25144
|
-
if (load !== null && this.scrubbing) this.scrubStats?.
|
|
25273
|
+
if (load !== null && this.scrubbing) this.scrubStats?.recordWarmRead(load.readBytes);
|
|
25145
25274
|
if (!this.warmLoopAlive(dragGen) || load === null) return null;
|
|
25146
25275
|
if (fillGen !== this.scrubWarmGen) return null;
|
|
25147
25276
|
this.scrubCache.put(this.deps.profile, load.seg);
|
|
@@ -25261,6 +25390,7 @@ var RecordedFeeder = class {
|
|
|
25261
25390
|
if (!this.scrubAckTimeoutLogged) {
|
|
25262
25391
|
this.scrubAckTimeoutLogged = true;
|
|
25263
25392
|
this.deps.onLog?.("scrub-feeder:ack-timeout", {
|
|
25393
|
+
dragId: this.scrubDragId,
|
|
25264
25394
|
pushes: expired,
|
|
25265
25395
|
frames,
|
|
25266
25396
|
timeoutMs: SCRUB_ACK_TIMEOUT_MS,
|
|
@@ -25292,6 +25422,7 @@ var RecordedFeeder = class {
|
|
|
25292
25422
|
if (!this.scrubParkLogged) {
|
|
25293
25423
|
this.scrubParkLogged = true;
|
|
25294
25424
|
this.deps.onLog?.("scrub-feeder:parked", {
|
|
25425
|
+
dragId: this.scrubDragId,
|
|
25295
25426
|
pendingEpochMs: this.scrubPendingEpochMs,
|
|
25296
25427
|
outstanding: this.scrubOutstanding.length
|
|
25297
25428
|
});
|
|
@@ -25324,7 +25455,7 @@ var RecordedFeeder = class {
|
|
|
25324
25455
|
seg = this.scrubCache.covering(this.deps.profile, target);
|
|
25325
25456
|
if (seg) {
|
|
25326
25457
|
coverage = "cache";
|
|
25327
|
-
if (this.scrubPrefetched.has(seg.startMs)) this.scrubStats?.
|
|
25458
|
+
if (this.scrubPrefetched.has(seg.startMs)) this.scrubStats?.recordWarmHit();
|
|
25328
25459
|
}
|
|
25329
25460
|
}
|
|
25330
25461
|
if (!seg) {
|
|
@@ -25354,6 +25485,7 @@ var RecordedFeeder = class {
|
|
|
25354
25485
|
if (!this.scrubDeadlineLogged) {
|
|
25355
25486
|
this.scrubDeadlineLogged = true;
|
|
25356
25487
|
this.deps.onLog?.("scrub-feeder:deadline-miss", {
|
|
25488
|
+
dragId: this.scrubDragId,
|
|
25357
25489
|
epochMs: target,
|
|
25358
25490
|
deadlineMs,
|
|
25359
25491
|
first: !this.scrubPushedThisDrag,
|
|
@@ -25477,6 +25609,7 @@ var RecordedFeeder = class {
|
|
|
25477
25609
|
if (this.scrubLastPushLogAtMs === 0 || pushedAtMs - this.scrubLastPushLogAtMs >= 1e3) {
|
|
25478
25610
|
this.scrubLastPushLogAtMs = pushedAtMs;
|
|
25479
25611
|
this.deps.onLog?.("scrub-feeder:pushed", {
|
|
25612
|
+
dragId: this.scrubDragId,
|
|
25480
25613
|
epochMs: effTarget,
|
|
25481
25614
|
mode,
|
|
25482
25615
|
dir,
|
|
@@ -25494,10 +25627,11 @@ var RecordedFeeder = class {
|
|
|
25494
25627
|
});
|
|
25495
25628
|
}
|
|
25496
25629
|
this.deps.onPosition(this.clampIntoSegment(seg, effTarget), pushSeq);
|
|
25497
|
-
this.
|
|
25630
|
+
this.scrubPrevTarget = effTarget;
|
|
25498
25631
|
} catch (err) {
|
|
25499
25632
|
this.scrubStats?.recordFailure();
|
|
25500
25633
|
this.deps.onLog?.("scrub-feeder:tick-failed", {
|
|
25634
|
+
dragId: this.scrubDragId,
|
|
25501
25635
|
epochMs: target,
|
|
25502
25636
|
error: err instanceof Error ? err.message : String(err)
|
|
25503
25637
|
});
|
|
@@ -25543,11 +25677,15 @@ var RecordedFeeder = class {
|
|
|
25543
25677
|
if (!this.scrubNoFootageLogged) {
|
|
25544
25678
|
this.scrubNoFootageLogged = true;
|
|
25545
25679
|
if (decision.kind === "too-far") this.deps.onLog?.("scrub-feeder:snap-too-far", {
|
|
25680
|
+
dragId: this.scrubDragId,
|
|
25546
25681
|
epochMs: target,
|
|
25547
25682
|
nearestMs: decision.snapMs,
|
|
25548
25683
|
distanceMs: decision.distanceMs
|
|
25549
25684
|
});
|
|
25550
|
-
else this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25685
|
+
else this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25686
|
+
dragId: this.scrubDragId,
|
|
25687
|
+
epochMs: target
|
|
25688
|
+
});
|
|
25551
25689
|
}
|
|
25552
25690
|
return null;
|
|
25553
25691
|
}
|
|
@@ -25563,11 +25701,15 @@ var RecordedFeeder = class {
|
|
|
25563
25701
|
if (snapped.kind !== "segment") {
|
|
25564
25702
|
if (!this.scrubNoFootageLogged) {
|
|
25565
25703
|
this.scrubNoFootageLogged = true;
|
|
25566
|
-
this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25704
|
+
this.deps.onLog?.("scrub-feeder:no-footage", {
|
|
25705
|
+
dragId: this.scrubDragId,
|
|
25706
|
+
epochMs: target
|
|
25707
|
+
});
|
|
25567
25708
|
}
|
|
25568
25709
|
return null;
|
|
25569
25710
|
}
|
|
25570
25711
|
this.deps.onLog?.("scrub-feeder:snapped", {
|
|
25712
|
+
dragId: this.scrubDragId,
|
|
25571
25713
|
requestedMs: target,
|
|
25572
25714
|
snappedMs: decision.snapMs
|
|
25573
25715
|
});
|
|
@@ -25700,11 +25842,12 @@ var RecordedFeeder = class {
|
|
|
25700
25842
|
*/
|
|
25701
25843
|
readsWholeSegment(loc) {
|
|
25702
25844
|
if (this.deps.profile !== SCRUB_WHOLE_SEGMENT_PROFILE) return false;
|
|
25703
|
-
if (loc.bytes === void 0 || loc.bytes <=
|
|
25845
|
+
if (loc.bytes === void 0 || loc.bytes <= 15e5) return true;
|
|
25704
25846
|
this.scrubStats?.recordOversizeSegment();
|
|
25705
25847
|
if (!this.scrubOversizeLogged) {
|
|
25706
25848
|
this.scrubOversizeLogged = true;
|
|
25707
25849
|
this.deps.onLog?.("scrub-feeder:oversize-segment", {
|
|
25850
|
+
dragId: this.scrubDragId,
|
|
25708
25851
|
profile: this.deps.profile,
|
|
25709
25852
|
startMs: loc.startMs,
|
|
25710
25853
|
durMs: loc.durMs,
|
|
@@ -25740,6 +25883,7 @@ var RecordedFeeder = class {
|
|
|
25740
25883
|
if (this.scrubProfileLogged) return;
|
|
25741
25884
|
this.scrubProfileLogged = true;
|
|
25742
25885
|
this.deps.onLog?.("scrub-feeder:profile-degraded", {
|
|
25886
|
+
dragId: this.scrubDragId,
|
|
25743
25887
|
profile: this.deps.profile,
|
|
25744
25888
|
scrubProfile: SCRUB_WHOLE_SEGMENT_PROFILE
|
|
25745
25889
|
});
|
|
@@ -25755,17 +25899,6 @@ var RecordedFeeder = class {
|
|
|
25755
25899
|
return sorted[Math.floor(sorted.length / 2)] ?? null;
|
|
25756
25900
|
}
|
|
25757
25901
|
/**
|
|
25758
|
-
* Is the disk the problem right now? The recent-read median against
|
|
25759
|
-
* {@link SCRUB_SLOW_READ_MS}. Median and not mean because one slow read is
|
|
25760
|
-
* noise and the decision must not swing on it; and never before a few
|
|
25761
|
-
* samples, or a cold first read would suppress the whole drag.
|
|
25762
|
-
*/
|
|
25763
|
-
scrubReadsAreSlow() {
|
|
25764
|
-
const median = this.scrubReadMedianMs();
|
|
25765
|
-
if (median === null) return false;
|
|
25766
|
-
return median >= (this.deps.slowReadMs ?? SCRUB_SLOW_READ_MS);
|
|
25767
|
-
}
|
|
25768
|
-
/**
|
|
25769
25902
|
* Fold one pointer sample into the drag's media velocity (ms of timeline per
|
|
25770
25903
|
* ms of wall clock, signed). Cheap on purpose: this runs at pointer rate.
|
|
25771
25904
|
*/
|
|
@@ -25801,7 +25934,7 @@ var RecordedFeeder = class {
|
|
|
25801
25934
|
if (Math.sign(velocity) !== Math.sign(this.scrubVelRun)) return 0;
|
|
25802
25935
|
const readMs = this.scrubReadMedianMs();
|
|
25803
25936
|
if (readMs === null) return 0;
|
|
25804
|
-
return
|
|
25937
|
+
return scrubLeadDecision(velocity, readMs).leadMs;
|
|
25805
25938
|
}
|
|
25806
25939
|
/**
|
|
25807
25940
|
* A single slow `locate` gets its own line, throttled per drag. Reserved for
|
|
@@ -25817,6 +25950,7 @@ var RecordedFeeder = class {
|
|
|
25817
25950
|
if (this.scrubLastSlowLocateLogAtMs !== 0 && now - this.scrubLastSlowLocateLogAtMs < SCRUB_PATHOLOGY_LOG_INTERVAL_MS) return;
|
|
25818
25951
|
this.scrubLastSlowLocateLogAtMs = now;
|
|
25819
25952
|
this.deps.onLog?.("scrub-feeder:slow-locate", {
|
|
25953
|
+
dragId: this.scrubDragId,
|
|
25820
25954
|
epochMs,
|
|
25821
25955
|
locateMs: Math.round(locateMs),
|
|
25822
25956
|
thresholdMs: threshold
|
|
@@ -25833,7 +25967,7 @@ var RecordedFeeder = class {
|
|
|
25833
25967
|
* interval comes out warm for the next drag. Aborts (without landing — the
|
|
25834
25968
|
* caller owns that) the moment a newer seek bumps the token.
|
|
25835
25969
|
*/
|
|
25836
|
-
async animateSeekTravel(fromMs, toMs, token) {
|
|
25970
|
+
async animateSeekTravel(fromMs, toMs, token, dragId) {
|
|
25837
25971
|
const cfg = this.deps.seekAnimation;
|
|
25838
25972
|
const budgetMs = cfg?.budgetMs ?? SEEK_ANIMATION_BUDGET_MS;
|
|
25839
25973
|
const tickMs = cfg?.tickMs ?? SEEK_ANIMATION_TICK_MS;
|
|
@@ -25865,6 +25999,7 @@ var RecordedFeeder = class {
|
|
|
25865
25999
|
if (this.disposed || token !== this.seekToken) return;
|
|
25866
26000
|
}
|
|
25867
26001
|
if (stills > 0) this.deps.onLog?.("recorded:seek-travel", {
|
|
26002
|
+
dragId,
|
|
25868
26003
|
fromMs,
|
|
25869
26004
|
toMs,
|
|
25870
26005
|
stills,
|
|
@@ -25959,79 +26094,41 @@ var RecordedFeeder = class {
|
|
|
25959
26094
|
return stills;
|
|
25960
26095
|
}
|
|
25961
26096
|
/**
|
|
25962
|
-
*
|
|
25963
|
-
* shows a sustained direction, kick off a single background prefetch of the
|
|
25964
|
-
* NEXT fragment in that direction. Prefetch fills the cache only — it never
|
|
25965
|
-
* pushes, so crossing into the neighbour later is an instant cache hit.
|
|
25966
|
-
*/
|
|
25967
|
-
maybeScrubPrefetch(seg, target, gen) {
|
|
25968
|
-
const prev = this.scrubPrevTarget;
|
|
25969
|
-
this.scrubPrevTarget = target;
|
|
25970
|
-
if (prev === null) {
|
|
25971
|
-
this.scrubDirRun = 0;
|
|
25972
|
-
return;
|
|
25973
|
-
}
|
|
25974
|
-
const stepMs = target - prev;
|
|
25975
|
-
const dir = stepMs > 0 ? 1 : stepMs < 0 ? -1 : 0;
|
|
25976
|
-
if (dir === 0) {
|
|
25977
|
-
this.scrubDirRun = 0;
|
|
25978
|
-
return;
|
|
25979
|
-
}
|
|
25980
|
-
this.scrubDirRun = Math.sign(this.scrubDirRun) === dir ? this.scrubDirRun + dir : dir;
|
|
25981
|
-
if (Math.abs(this.scrubDirRun) < SCRUB_PREFETCH_DIR_THRESHOLD) return;
|
|
25982
|
-
if (this.scrubReadsAreSlow()) {
|
|
25983
|
-
this.scrubStats?.recordPrefetchSuppressed();
|
|
25984
|
-
return;
|
|
25985
|
-
}
|
|
25986
|
-
if (this.scrubFillLoopActive) return;
|
|
25987
|
-
const aheadMs = Math.max(Math.abs(stepMs), seg.durMs + 1);
|
|
25988
|
-
const horizons = Math.abs(stepMs) > seg.durMs ? SCRUB_PREFETCH_MAX_INFLIGHT : 1;
|
|
25989
|
-
for (let horizon = 1; horizon <= horizons; horizon++) {
|
|
25990
|
-
if (this.scrubPrefetchInFlight >= SCRUB_PREFETCH_MAX_INFLIGHT) return;
|
|
25991
|
-
const probeMs = target + dir * aheadMs * horizon;
|
|
25992
|
-
if (probeMs < 0) continue;
|
|
25993
|
-
if (this.scrubPrefetchProbes.has(probeMs)) continue;
|
|
25994
|
-
if (this.scrubCache.covering(this.deps.profile, probeMs)) continue;
|
|
25995
|
-
if (this.scrubWarmCentreMs !== null && Math.abs(probeMs - this.scrubWarmCentreMs) > SCRUB_WARM_AROUND_MS) continue;
|
|
25996
|
-
this.scrubPrefetch(probeMs, gen);
|
|
25997
|
-
}
|
|
25998
|
-
}
|
|
25999
|
-
/**
|
|
26000
|
-
* Speculative read: locate + read + demux the fragment covering `probeMs` into
|
|
26001
|
-
* `scrubCache`. Bounded by {@link SCRUB_PREFETCH_MAX_INFLIGHT}; aborts at any
|
|
26002
|
-
* teardown (`disposed` / left scrub / a bumped `scrubToken`); a failure is
|
|
26003
|
-
* swallowed (the fragment just demuxes on demand). Never pushes and never
|
|
26004
|
-
* mutates latest-wins state.
|
|
26097
|
+
* The directional prefetch is GONE (2026-08-26), not disabled.
|
|
26005
26098
|
*
|
|
26006
|
-
*
|
|
26007
|
-
*
|
|
26008
|
-
*
|
|
26009
|
-
*
|
|
26010
|
-
|
|
26011
|
-
|
|
26012
|
-
|
|
26013
|
-
|
|
26014
|
-
|
|
26015
|
-
|
|
26016
|
-
|
|
26017
|
-
|
|
26018
|
-
|
|
26019
|
-
|
|
26020
|
-
|
|
26021
|
-
|
|
26022
|
-
|
|
26023
|
-
|
|
26024
|
-
|
|
26025
|
-
|
|
26026
|
-
|
|
26027
|
-
|
|
26028
|
-
|
|
26029
|
-
|
|
26030
|
-
|
|
26031
|
-
|
|
26032
|
-
|
|
26033
|
-
|
|
26034
|
-
|
|
26099
|
+
* It guessed where the next tick would land from the drag's recent
|
|
26100
|
+
* direction. Measured on the operator's own drags the day it was removed:
|
|
26101
|
+
* **25 speculative reads bought 3 hits** — 88% waste, and roughly 60% of all
|
|
26102
|
+
* disk work during a gesture, queued as random seeks ahead of the read the
|
|
26103
|
+
* finger was actually blocked on, on a 5400 rpm SMR disk.
|
|
26104
|
+
*
|
|
26105
|
+
* The regression tests it passed drove a drag at CONSTANT step size, which is
|
|
26106
|
+
* the one regime where direction predicts position. A real backward scrub is
|
|
26107
|
+
* exploratory — 26 of 29 jumps backward, step size set by where the operator
|
|
26108
|
+
* decided to look next — so the premise never held in the field.
|
|
26109
|
+
*
|
|
26110
|
+
* Scrypted has no predictive prefetch either: their read-ahead is a stream
|
|
26111
|
+
* `highWaterMark`, and a short forward move is a fast-forward on an open fd
|
|
26112
|
+
* rather than a seek. They deleted the problem instead of solving it.
|
|
26113
|
+
*
|
|
26114
|
+
* What REMAINS and must not be confused with this: the WINDOW warm
|
|
26115
|
+
* (`runScrubFillLoop`) — a sequential walk of the window around the playhead.
|
|
26116
|
+
*
|
|
26117
|
+
* It was described here, wrongly, as running "when no drag is live, so it
|
|
26118
|
+
* competes with nothing". It does not: its survival condition is
|
|
26119
|
+
* `this.scrubbing || this.idleWarm`, and within hours of this deletion it had
|
|
26120
|
+
* inherited the deleted predictor's role — 1.9 MB and 2.4 MB of reads on two
|
|
26121
|
+
* measured drags, 38-45% of the gesture's I/O, zero cache hits. Deleting one
|
|
26122
|
+
* speculator without looking at the other left the contention exactly where
|
|
26123
|
+
* it was. It now waits out a moving finger (`fingerIsMoving`), which is what
|
|
26124
|
+
* the sentence above claimed all along.
|
|
26125
|
+
*
|
|
26126
|
+
* Cost of the removal, bounded and accepted: a careful short-step drag no
|
|
26127
|
+
* longer warms the fragment across the edge, so the first tick past a
|
|
26128
|
+
* boundary may pay one cold read. Its own regression test shows that case is
|
|
26129
|
+
* served by the MEMO (`coverMemo ≥ 5`) with the neighbour warm worth ≤2
|
|
26130
|
+
* reads.
|
|
26131
|
+
*/
|
|
26035
26132
|
/**
|
|
26036
26133
|
* Feed exactly one frame forward (+1) or backward (-1). Only meaningful while
|
|
26037
26134
|
* paused over a segment (rate 0, recorded mode) — ignored otherwise. Forward
|
|
@@ -26215,7 +26312,7 @@ var RecordedFeeder = class {
|
|
|
26215
26312
|
this.advanceLoop(gen, initial).catch(() => {
|
|
26216
26313
|
if (!this.disposed && gen === this.advanceToken) {
|
|
26217
26314
|
this.stopPositionReports();
|
|
26218
|
-
this.
|
|
26315
|
+
this.emitState("error", "advance-loop-failed", this.seekToken, this.scrubDragId);
|
|
26219
26316
|
}
|
|
26220
26317
|
});
|
|
26221
26318
|
}
|
|
@@ -26232,7 +26329,7 @@ var RecordedFeeder = class {
|
|
|
26232
26329
|
if (prepared === null) return;
|
|
26233
26330
|
if (prepared === "ended") {
|
|
26234
26331
|
this.stopPositionReports();
|
|
26235
|
-
this.
|
|
26332
|
+
this.emitState("ended", "advance-ended", this.seekToken, this.scrubDragId);
|
|
26236
26333
|
return;
|
|
26237
26334
|
}
|
|
26238
26335
|
if (isResumedSegment) {} else {
|
|
@@ -26509,7 +26606,10 @@ var RecordedFeeder = class {
|
|
|
26509
26606
|
const mark = this.scrubDemuxMark;
|
|
26510
26607
|
const demux = mark && this.deps.demuxTally ? this.deps.demuxTally.deltaSince(mark) : void 0;
|
|
26511
26608
|
this.scrubDemuxMark = null;
|
|
26512
|
-
this.deps.onLog?.("scrub-feeder:drag-end",
|
|
26609
|
+
this.deps.onLog?.("scrub-feeder:drag-end", {
|
|
26610
|
+
dragId: this.scrubDragId,
|
|
26611
|
+
...stats.toLogFields(performance.now() - this.scrubStartedAtMs, demux)
|
|
26612
|
+
});
|
|
26513
26613
|
}
|
|
26514
26614
|
/** Stop everything: invalidate in-flight work and tear down the position timer. */
|
|
26515
26615
|
dispose() {
|
|
@@ -26522,7 +26622,6 @@ var RecordedFeeder = class {
|
|
|
26522
26622
|
this.scrubToken++;
|
|
26523
26623
|
this.scrubSegment = null;
|
|
26524
26624
|
this.scrubPrevTarget = null;
|
|
26525
|
-
this.scrubDirRun = 0;
|
|
26526
26625
|
this.scrubCursor = null;
|
|
26527
26626
|
this.scrubNoFootageLogged = false;
|
|
26528
26627
|
this.scrubParkLogged = false;
|
|
@@ -26614,10 +26713,11 @@ var TimelineSession = class {
|
|
|
26614
26713
|
await this.playAt(msg.epoch, this.profile, { travelFromMs: this.lastPositionMs });
|
|
26615
26714
|
break;
|
|
26616
26715
|
case "scrubCommit":
|
|
26617
|
-
await this.playAt(msg.epoch, this.profile);
|
|
26716
|
+
if (msg.dragId !== void 0) await this.playAt(msg.epoch, this.profile, { dragId: msg.dragId });
|
|
26717
|
+
else await this.playAt(msg.epoch, this.profile);
|
|
26618
26718
|
break;
|
|
26619
26719
|
case "scrubStart":
|
|
26620
|
-
(this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).beginScrub();
|
|
26720
|
+
(this.mode === "recorded" && this.feeder ? this.feeder : this.enterRecorded()).beginScrub(msg.dragId);
|
|
26621
26721
|
break;
|
|
26622
26722
|
case "scrubTo": {
|
|
26623
26723
|
const hint = msg.m !== void 0 && msg.v !== void 0 ? {
|
|
@@ -26871,7 +26971,7 @@ function createOffThreadJsParse(deps) {
|
|
|
26871
26971
|
if (!w) return deps.parseInProcess(bytes);
|
|
26872
26972
|
const id = nextId;
|
|
26873
26973
|
nextId += 1;
|
|
26874
|
-
const copy = bytes
|
|
26974
|
+
const copy = new Uint8Array(bytes);
|
|
26875
26975
|
const request = {
|
|
26876
26976
|
id,
|
|
26877
26977
|
bytes: copy
|
|
@@ -28308,14 +28408,21 @@ var BrokerWebrtcServer = class {
|
|
|
28308
28408
|
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
28309
28409
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
28310
28410
|
},
|
|
28311
|
-
onState: (
|
|
28312
|
-
sessionLogger.info("recorded: feeder state", {
|
|
28313
|
-
|
|
28314
|
-
|
|
28315
|
-
|
|
28316
|
-
|
|
28317
|
-
|
|
28318
|
-
|
|
28411
|
+
onState: (t) => {
|
|
28412
|
+
sessionLogger.info("recorded: feeder state", {
|
|
28413
|
+
tags: { deviceId },
|
|
28414
|
+
meta: {
|
|
28415
|
+
sessionId,
|
|
28416
|
+
deviceId,
|
|
28417
|
+
profile,
|
|
28418
|
+
from: t.from,
|
|
28419
|
+
to: t.to,
|
|
28420
|
+
reason: t.reason,
|
|
28421
|
+
seekToken: t.seekToken,
|
|
28422
|
+
dragId: t.dragId
|
|
28423
|
+
}
|
|
28424
|
+
});
|
|
28425
|
+
timeline._emitState(t.to);
|
|
28319
28426
|
},
|
|
28320
28427
|
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
28321
28428
|
onLog: (msg, data) => sessionLogger.info(msg, {
|