@torrent-tv/proxy 2.107.53 → 2.107.55

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.
@@ -42,6 +42,9 @@ import { startMemoryReport, WORKER_MEMORY_SAMPLE_MS } from "../../storage/memory
42
42
  import { profileThisThread } from "../../storage/cpu-profile.js";
43
43
  import { forwardLogsTo, logger } from "../../../utils/logger.js";
44
44
 
45
+ const WORKER_CLOCK_EPOCH = performance.timeOrigin;
46
+ let sourceSnapshotRevision = 0;
47
+
45
48
  /**
46
49
  * Everything this thread logs goes to the main thread, which owns the file.
47
50
  *
@@ -492,6 +495,8 @@ async function runCommand(command, params, id) {
492
495
 
493
496
  case Command.FILE_STATS: {
494
497
  const statsStartedAt = performance.now();
498
+ const sourceCaptureStartedAt = statsStartedAt;
499
+ const sourceRevision = ++sourceSnapshotRevision;
495
500
  emitWaitTrace(params.sourceKey, "W10", "begin", { startedAt: statsStartedAt,
496
501
  fileIndex: params.fileIndex, forecast: params.forecast === true });
497
502
  const finishStatsTrace = (outcome, fields = {}) => {
@@ -504,7 +509,14 @@ async function runCommand(command, params, id) {
504
509
  if (!torrent) {
505
510
  const whole = wholeSources.fileOf(params.sourceKey, params.fileIndex);
506
511
  finishStatsTrace(whole ? "whole-file" : "pending");
507
- return whole ? wholeFileStats(whole.length) : { pending: true };
512
+ return {
513
+ ...(whole ? wholeFileStats(whole.length) : { pending: true }),
514
+ sourceCapture: {
515
+ owner: "torrent-worker", clock: "performance", clockEpoch: WORKER_CLOCK_EPOCH,
516
+ startedAt: sourceCaptureStartedAt, completedAt: performance.now(), revision: sourceRevision,
517
+ validity: whole ? "valid" : "unknown", unknownReason: whole ? null : "source-not-ready"
518
+ }
519
+ };
508
520
  }
509
521
  // What this file's own interruptions demand, measured by the reader in
510
522
  // this thread. It travels with the stats because the caller asking for
@@ -515,7 +527,8 @@ async function runCommand(command, params, id) {
515
527
  const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
516
528
  const forecastAt = performance.now();
517
529
  const forecast = file && params.forecast === true ? downloadForecastOf(torrent) : null;
518
- const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
530
+ const forecastCompletedAt = performance.now();
531
+ const forecastMs = params.forecast === true ? forecastCompletedAt - forecastAt : 0;
519
532
  const stats = pool.getFileStats(torrent, params.fileIndex);
520
533
  const ranges = [];
521
534
  if (forecast) for (const [piece, availableAt] of forecast.arrivals) {
@@ -526,8 +539,19 @@ async function runCommand(command, params, id) {
526
539
  finishStatsTrace("ok", { forecast: params.forecast === true });
527
540
  return {
528
541
  ...stats,
542
+ sourceCapture: {
543
+ owner: "torrent-worker", clock: "performance", clockEpoch: WORKER_CLOCK_EPOCH,
544
+ startedAt: sourceCaptureStartedAt, completedAt: performance.now(), revision: sourceRevision,
545
+ validity: stats ? "valid" : "unknown", unknownReason: stats ? null : "source-stats-unavailable"
546
+ },
529
547
  ...(params.forecast === true ? { downloadForecast: {
530
- measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges
548
+ measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges,
549
+ capture: {
550
+ owner: "torrent-worker", clock: "performance", clockEpoch: WORKER_CLOCK_EPOCH,
551
+ startedAt: forecastAt, completedAt: forecastCompletedAt, revision: sourceRevision,
552
+ validity: forecast ? "valid" : "unknown",
553
+ unknownReason: forecast ? null : "source-arrival-forecast-unavailable"
554
+ }
531
555
  } } : {}),
532
556
  supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4),
533
557
  // How long this answer waited for the arrival forecast, which the
@@ -27,10 +27,11 @@ export class OutputForecastGeometry {
27
27
  const count = timeline?.segmentCount ?? 0;
28
28
  let forecast = this.#byOutput.get(output);
29
29
  if (!forecast) {
30
- forecast = { pieces: [], geometry: {} };
30
+ forecast = { pieces: [], tailPieces: [], tailSegments: [], geometry: {} };
31
31
  this.#byOutput.set(output, forecast);
32
32
  }
33
33
  const kept = forecast.pieces;
34
+ const keptTail = forecast.tailPieces;
34
35
  const segments = [];
35
36
  const lookahead = Number(this.#host.lookaheadSeconds);
36
37
  const start = Number.isFinite(positionSeconds) ? Math.max(0, positionSeconds) : 0;
@@ -50,6 +51,23 @@ export class OutputForecastGeometry {
50
51
  else high = middle;
51
52
  }
52
53
  const last = low;
54
+ const tailSegments = [];
55
+ for (let index = last; index < count; index += 1) {
56
+ const startSeconds = timeline.publishedStartOf(index);
57
+ const endSeconds = timeline.publishedStartOf(index + 1);
58
+ const sourceInputs = this.#host.sourceInputsFor?.(output, index) ?? undefined;
59
+ const previous = keptTail[index];
60
+ if (previous && previous.startSeconds === startSeconds && previous.endSeconds === endSeconds &&
61
+ sameInputs(previous.sourceInputs, sourceInputs)) {
62
+ tailSegments.push(previous);
63
+ continue;
64
+ }
65
+ const segment = { index, startSeconds, endSeconds, sourceInputs };
66
+ keptTail[index] = segment;
67
+ tailSegments.push(segment);
68
+ }
69
+ if (keptTail.length > count) keptTail.length = count;
70
+ forecast.tailSegments = tailSegments;
53
71
  const indexes = new Set([0]);
54
72
  for (let index = first; index < last; index++) indexes.add(index);
55
73
  for (const index of indexes) {
@@ -53,6 +53,34 @@ function fragmentEventAges(value) {
53
53
  Number.isFinite(value?.[name]) && value[name] >= 0 ? value[name] : null]));
54
54
  }
55
55
 
56
+ function browserCaptureOf(report, sessionId) {
57
+ const clockEpoch = report.captureClockEpoch;
58
+ const startedAt = report.captureStartedAtBrowser;
59
+ const completedAt = report.captureCompletedAtBrowser;
60
+ const valid = typeof report.captureEpoch === "string" && report.captureEpoch.length > 0 &&
61
+ Number.isFinite(report.captureEpochStartedAtBrowser) && Number.isFinite(report.captureRevision) &&
62
+ Number.isSafeInteger(report.captureRevision) && report.captureRevision > 0 &&
63
+ Number.isFinite(clockEpoch) && Number.isFinite(startedAt) && Number.isFinite(completedAt) &&
64
+ report.captureEpochStartedAtBrowser >= clockEpoch && startedAt >= report.captureEpochStartedAtBrowser &&
65
+ completedAt >= startedAt &&
66
+ Number.isSafeInteger(report.generation) && report.generation >= 0;
67
+ return {
68
+ owner: "browser",
69
+ clock: "performance",
70
+ clockEpoch: Number.isFinite(clockEpoch) ? clockEpoch : null,
71
+ startedAt: Number.isFinite(startedAt) ? startedAt : null,
72
+ completedAt: Number.isFinite(completedAt) ? completedAt : null,
73
+ actionId: typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null,
74
+ generation: Number.isSafeInteger(report.generation) ? report.generation : null,
75
+ revision: Number.isSafeInteger(report.captureRevision) ? report.captureRevision : null,
76
+ epoch: typeof report.captureEpoch === "string" ? report.captureEpoch : null,
77
+ epochStartedAt: Number.isFinite(report.captureEpochStartedAtBrowser)
78
+ ? report.captureEpochStartedAtBrowser : null,
79
+ validity: valid ? "valid" : "unknown",
80
+ unknownReason: valid ? null : "missing-or-invalid-browser-capture"
81
+ };
82
+ }
83
+
56
84
  function appendServiceSnapshot(value) {
57
85
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
58
86
  const snapshot = {};
@@ -522,6 +550,18 @@ export class Viewer {
522
550
  appendEpoch = null,
523
551
  appendEpochStartedAt = null,
524
552
  appendSnapshotAt = null,
553
+ captureClockEpoch = null,
554
+ captureEpoch = null,
555
+ captureEpochStartedAtBrowser = null,
556
+ captureRevision = null,
557
+ captureStartedAtBrowser = null,
558
+ captureCompletedAtBrowser = null,
559
+ linkSampleStartedAtBrowser = null,
560
+ linkSampleCompletedAtBrowser = null,
561
+ linkSampleActionId = null,
562
+ linkSampleGeneration = null,
563
+ linkSampleClockEpoch = null,
564
+ linkSampleRevision = null,
525
565
  appendService: appendServiceReading = null,
526
566
  fragmentService: fragmentServiceReading = null,
527
567
  bufferedAheadSec,
@@ -564,6 +604,18 @@ export class Viewer {
564
604
  const sampleMbps = Number(linkSampleMbps);
565
605
  const sampleAt = Number(linkSampleAt);
566
606
  const previous = this.netReport;
607
+ const browserCapture = browserCaptureOf({ captureClockEpoch, captureEpoch, captureEpochStartedAtBrowser,
608
+ captureRevision, captureStartedAtBrowser, captureCompletedAtBrowser, generation }, appendSessionId);
609
+ const previousCapture = previous?.bufferCapture ?? null;
610
+ const captureOrderKnown = browserCapture.clockEpoch !== null && browserCapture.epoch !== null &&
611
+ browserCapture.epochStartedAt !== null && browserCapture.revision !== null && browserCapture.revision > 0;
612
+ const sameCaptureClock = captureOrderKnown && browserCapture.clockEpoch === previousCapture?.clockEpoch;
613
+ const sameCaptureEpoch = sameCaptureClock && browserCapture.epoch === previousCapture?.epoch;
614
+ const newerCapture = captureOrderKnown && (!previousCapture || !previousCapture.clockEpoch ||
615
+ !sameCaptureClock && browserCapture.epochStartedAt > (previousCapture.epochStartedAt ?? 0) ||
616
+ sameCaptureClock && (sameCaptureEpoch && browserCapture.revision > previousCapture.revision ||
617
+ !sameCaptureEpoch && browserCapture.epochStartedAt > (previousCapture.epochStartedAt ?? 0)));
618
+ const staleCapture = captureOrderKnown && previousCapture && !newerCapture;
567
619
  const incomingAppendService = typeof appendEpoch === "string" && appendEpoch.length > 0
568
620
  ? appendServiceSnapshot(appendServiceReading) : null;
569
621
  const incomingFragmentService = typeof appendEpoch === "string" && appendEpoch.length > 0
@@ -578,37 +630,85 @@ export class Viewer {
578
630
  (!previous?.appendService || appendServiceIsMonotonic(incomingAppendService, previous.appendService)) &&
579
631
  (!previous?.fragmentService || fragmentServiceIsMonotonic(incomingFragmentService, previous.fragmentService))
580
632
  : appendEpochStartedAt > (previous?.appendEpochStartedAt ?? 0));
633
+ const preserveBrowserService = sameAppendEpoch && incomingAppendService && incomingFragmentService;
581
634
  const hasPlaybackSample = Number.isFinite(sampleMbps) && sampleMbps > 0 &&
582
- Number.isFinite(sampleAt) && sampleAt > 0;
583
- const newerPlaybackSample = hasPlaybackSample && sampleAt > (this.netReport?.linkSampleAt ?? 0);
635
+ Number.isFinite(sampleAt) && sampleAt > 0 && Number.isFinite(captureClockEpoch) &&
636
+ Number.isFinite(linkSampleClockEpoch) && linkSampleClockEpoch === captureClockEpoch &&
637
+ Number.isSafeInteger(linkSampleRevision) && linkSampleRevision > 0 &&
638
+ Number.isFinite(linkSampleStartedAtBrowser) && Number.isFinite(linkSampleCompletedAtBrowser) &&
639
+ linkSampleStartedAtBrowser >= captureClockEpoch && linkSampleCompletedAtBrowser === sampleAt &&
640
+ linkSampleStartedAtBrowser <= linkSampleCompletedAtBrowser &&
641
+ Number.isFinite(captureCompletedAtBrowser) && sampleAt <= captureCompletedAtBrowser &&
642
+ Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0;
643
+ const oldLinkEpoch = this.netReport?.linkCapture?.epoch !== String(linkSampleClockEpoch) &&
644
+ Number.isFinite(this.netReport?.linkCapture?.epochStartedAt) &&
645
+ Number.isFinite(linkSampleClockEpoch) &&
646
+ linkSampleClockEpoch <= this.netReport.linkCapture.epochStartedAt;
647
+ const newerPlaybackSample = hasPlaybackSample && !oldLinkEpoch &&
648
+ (linkSampleClockEpoch !== this.netReport?.linkCapture?.clockEpoch ||
649
+ linkSampleRevision > (this.netReport?.linkCapture?.revision ?? 0));
650
+ const linkClockEpochChanged = Number.isFinite(captureClockEpoch) &&
651
+ Number.isFinite(this.netReport?.linkCapture?.clockEpoch) &&
652
+ captureClockEpoch !== this.netReport.linkCapture.clockEpoch;
653
+ const newerInvalidPlaybackSample = !stale && newerCapture && !hasPlaybackSample &&
654
+ (Number.isSafeInteger(linkSampleRevision) || linkSampleAt !== null && linkSampleAt !== undefined ||
655
+ linkClockEpochChanged);
656
+ const preserveCapture = stale || staleCapture;
657
+ const linkCapture = newerPlaybackSample ? {
658
+ owner: "browser", clock: "performance", clockEpoch: linkSampleClockEpoch,
659
+ startedAt: linkSampleStartedAtBrowser, completedAt: linkSampleCompletedAtBrowser,
660
+ actionId: typeof linkSampleActionId === "string" ? linkSampleActionId : null,
661
+ generation: Number.isSafeInteger(linkSampleGeneration) ? linkSampleGeneration : null,
662
+ epoch: String(linkSampleClockEpoch), epochStartedAt: linkSampleClockEpoch,
663
+ revision: linkSampleRevision, validity: "valid", unknownReason: null
664
+ } : newerInvalidPlaybackSample ? {
665
+ owner: "browser", clock: "performance", clockEpoch: Number.isFinite(captureClockEpoch) ? captureClockEpoch : null,
666
+ startedAt: null, completedAt: null, actionId: appendSessionId,
667
+ generation: Number.isSafeInteger(generation) ? generation : null,
668
+ epoch: Number.isFinite(captureClockEpoch) ? String(captureClockEpoch) : null,
669
+ epochStartedAt: captureClockEpoch,
670
+ revision: linkSampleRevision, validity: "invalid", unknownReason: "invalid-link-sample-capture"
671
+ } : this.netReport?.linkCapture ?? {
672
+ owner: "browser", clock: "performance", clockEpoch: null, startedAt: null, completedAt: null,
673
+ actionId: appendSessionId, generation: Number.isSafeInteger(generation) ? generation : null,
674
+ epoch: null, epochStartedAt: null, revision: null, validity: "unknown",
675
+ unknownReason: "missing-link-sample-capture"
676
+ };
677
+ const useCapture = !stale && !staleCapture && newerCapture && browserCapture.validity === "valid";
584
678
  this.netReport = {
585
679
  linkMbps: lastLinkMbps,
586
680
  linkMeasuredAt,
587
- linkSampleAt: newerPlaybackSample ? sampleAt : this.netReport?.linkSampleAt ?? null,
588
- linkSampleMbps: newerPlaybackSample ? sampleMbps : this.netReport?.linkSampleMbps ?? null,
589
- linkSampleMeasuredAt: newerPlaybackSample
590
- ? Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0
591
- ? now - linkSampleAgeMs
592
- : now
593
- : this.netReport?.linkSampleMeasuredAt ?? null,
681
+ linkSampleAt: newerPlaybackSample ? sampleAt : newerInvalidPlaybackSample ? null : this.netReport?.linkSampleAt ?? null,
682
+ linkSampleMbps: newerPlaybackSample ? sampleMbps : newerInvalidPlaybackSample ? null : this.netReport?.linkSampleMbps ?? null,
683
+ linkSampleMeasuredAt: newerPlaybackSample ? now : newerInvalidPlaybackSample ? null : this.netReport?.linkSampleMeasuredAt ?? null,
684
+ linkCapture,
685
+ bufferCapture: preserveCapture ? previousCapture ?? browserCapture : useCapture ? browserCapture : {
686
+ ...browserCapture,
687
+ validity: "unknown",
688
+ unknownReason: browserCapture.unknownReason ?? "missing-or-invalid-browser-capture"
689
+ },
594
690
  appendSessionId: useBrowserService ? appendSessionId : previous?.appendSessionId ?? null,
595
- appendEpoch: useBrowserService ? appendEpoch : previous?.appendEpoch ?? null,
691
+ appendEpoch: useBrowserService ? appendEpoch : preserveBrowserService ? previous?.appendEpoch ?? null
692
+ : useCapture ? null : previous?.appendEpoch ?? null,
596
693
  appendEpochStartedAt: useBrowserService ? appendEpochStartedAt : previous?.appendEpochStartedAt ?? null,
597
694
  appendSnapshotAt: useBrowserService ? appendSnapshotAt : previous?.appendSnapshotAt ?? null,
598
- appendService: useBrowserService ? incomingAppendService : previous?.appendService ?? null,
599
- fragmentService: useBrowserService ? incomingFragmentService : previous?.fragmentService ?? null,
600
- appendServiceReceivedAt: useBrowserService ? now : previous?.appendServiceReceivedAt ?? null,
601
- bufferedAheadSec: held,
695
+ appendService: useBrowserService ? incomingAppendService : preserveBrowserService ? previous?.appendService ?? null
696
+ : useCapture ? null : previous?.appendService ?? null,
697
+ fragmentService: useBrowserService ? incomingFragmentService : preserveBrowserService ? previous?.fragmentService ?? null
698
+ : useCapture ? null : previous?.fragmentService ?? null,
699
+ appendServiceReceivedAt: useBrowserService ? now : preserveBrowserService ? previous?.appendServiceReceivedAt ?? null
700
+ : useCapture ? null : previous?.appendServiceReceivedAt ?? null,
701
+ bufferedAheadSec: preserveCapture ? previous?.bufferedAheadSec ?? 0 : useCapture ? held : 0,
602
702
  timestampOffsets: Object.fromEntries(["video", "audio"].filter((key) =>
603
703
  Number.isFinite(timestampOffsets?.[key])).map((key) => [key, timestampOffsets[key]])),
604
- bufferedRanges: bufferedRanges && typeof bufferedRanges === "object" ?
704
+ bufferedRanges: preserveCapture ? previous?.bufferedRanges ?? null : useCapture && bufferedRanges && typeof bufferedRanges === "object" ?
605
705
  Object.fromEntries(["video", "audio", "media"].filter((key) => Array.isArray(bufferedRanges[key]))
606
706
  .map((key) => [key, bufferedRanges[key].slice(0, 256).filter((range) =>
607
707
  Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.start >= 0 && range.end > range.start)
608
708
  .map(({ start, end }) => ({ start, end }))])) : null,
609
- bufferLimitSeconds: Number.isFinite(bufferLimitSeconds) && bufferLimitSeconds > 0
709
+ bufferLimitSeconds: preserveCapture ? previous?.bufferLimitSeconds ?? null : useCapture && Number.isFinite(bufferLimitSeconds) && bufferLimitSeconds > 0
610
710
  ? bufferLimitSeconds
611
- : this.netReport?.bufferLimitSeconds ?? null,
711
+ : useCapture ? null : this.netReport?.bufferLimitSeconds ?? null,
612
712
  positionSeconds:
613
713
  Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : null,
614
714
  at: now
@@ -46,11 +46,15 @@ export class ViewerPlaybackState {
46
46
  return removed;
47
47
  }
48
48
 
49
- addLinkReading(state, at, mbps) {
50
- state.link.add(at, mbps * 1_000_000);
49
+ addLinkReading(state, at, mbps, capture = null) {
50
+ state.link.add(at, mbps * 1_000_000, capture);
51
51
  }
52
52
 
53
- sourceRateReadings(state, id, stats, now, playbackReadiness, lookaheadSeconds) {
53
+ forgetLinkReadings(state, playbackReadiness, lookaheadSeconds) {
54
+ if (state) state.link = new playbackReadiness.RateTrend(lookaheadSeconds);
55
+ }
56
+
57
+ sourceRateReadings(state, id, stats, now, playbackReadiness, lookaheadSeconds, capture = null) {
54
58
  let source = state.sources.get(id);
55
59
  if (!source) {
56
60
  const { RateTrend, SessionRate } = playbackReadiness;
@@ -58,15 +62,16 @@ export class ViewerPlaybackState {
58
62
  deliveredAll: new SessionRate(), freeAll: new SessionRate(), limit: null };
59
63
  state.sources.set(id, source);
60
64
  }
65
+ const readingCapture = capture ?? stats?.sourceCapture ?? null;
61
66
  if (Number.isFinite(stats?.downloadSpeed) && stats.downloadSpeed >= 0) {
62
- source.delivered.add(now, stats.downloadSpeed);
63
- source.deliveredAll.add(now, stats.downloadSpeed);
67
+ source.delivered.add(now, stats.downloadSpeed, readingCapture);
68
+ source.deliveredAll.add(now, stats.downloadSpeed, readingCapture);
64
69
  }
65
70
  const limit = stats?.selectionLimit;
66
71
  if (limit && source.limit && limit.freeMs > source.limit.freeMs) {
67
72
  const free = Math.max(0, limit.freeBytes - source.limit.freeBytes) * 1000 / (limit.freeMs - source.limit.freeMs);
68
- source.free.add(now, free);
69
- source.freeAll.add(now, free);
73
+ source.free.add(now, free, readingCapture);
74
+ source.freeAll.add(now, free, readingCapture);
70
75
  }
71
76
  if (limit) source.limit = limit;
72
77
  return sourceService(source.free.snapshot(), source.delivered.snapshot(),
@@ -85,6 +90,19 @@ export class ViewerPlaybackState {
85
90
  return source ? { free: source.free.snapshot(), delivered: source.delivered.snapshot() } : null;
86
91
  }
87
92
 
93
+ sourceCaptureSnapshot(state, id) {
94
+ const source = state.sources.get(id);
95
+ if (!source) return null;
96
+ return {
97
+ recent: { delivered: source.delivered.captureSnapshot(), free: source.free.captureSnapshot() },
98
+ tail: { delivered: source.deliveredAll.captureSnapshot(), free: source.freeAll.captureSnapshot() }
99
+ };
100
+ }
101
+
102
+ forgetSourceReadings(state, id) {
103
+ state?.sources.delete(id);
104
+ }
105
+
88
106
  getWait(viewer, waitId) {
89
107
  return this.#stateFor(viewer)?.waits.get(waitId) ?? null;
90
108
  }