@torrent-tv/proxy 2.107.30 → 2.107.32
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/package.json
CHANGED
|
@@ -134,7 +134,8 @@ export class RateTrend {
|
|
|
134
134
|
* @param {Array<{ id: string, serviceKey?: string, complete: boolean, fileOffset?: number,
|
|
135
135
|
* fileLength?: number, bytesPerMediaSecond?: number, pieceLength?: number, downloadRateReadings?: object,
|
|
136
136
|
* downloadForecast?: { ranges: Array<{ start: number, end: number, availableAt: number | null }> } }>} input.sources
|
|
137
|
-
* @param {Array<{ id: string, sourceIds: string[], processedSeconds: number,
|
|
137
|
+
* @param {Array<{ id: string, sourceIds: string[], processedSeconds: number, durationSeconds?: number,
|
|
138
|
+
* segmentCount?: number,
|
|
138
139
|
* bitsPerMediaSecond: number, readings: Array<{ at: number, value: number }>,
|
|
139
140
|
* clockOffsetSeconds?: number, clientRanges?: Array<{ start: number, end: number }>,
|
|
140
141
|
* segments: Array<{ index: number, startSeconds: number, endSeconds: number,
|
|
@@ -157,16 +158,17 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
157
158
|
// starts), the read piece nearest before them (what an unread piece is placed
|
|
158
159
|
// by) and read pieces inside what the page reports holding (their frames).
|
|
159
160
|
//
|
|
160
|
-
// THE NEAR WINDOW
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
161
|
+
// THE NEAR WINDOW. The look-ahead is scheduled with exact segment times and
|
|
162
|
+
// source ranges when known. Missing range metadata is priced from aggregate
|
|
163
|
+
// file density and measured download service without claiming an address or
|
|
164
|
+
// byte. This forecast decides whether playback can start from work near the
|
|
165
|
+
// viewer; it does not make the first frame wait on an aggregate estimate of
|
|
166
|
+
// the whole remaining film. Each later poll advances this same window.
|
|
165
167
|
const lookahead = finiteNonNegative(input.lookaheadSeconds);
|
|
166
168
|
const horizonEnd = lookahead > 0 ? finiteNonNegative(input.positionSeconds) + lookahead : Number.POSITIVE_INFINITY;
|
|
167
169
|
const tracks = (Array.isArray(input.tracks) ? input.tracks : []).map((track) =>
|
|
168
|
-
Array.isArray(track.segments)
|
|
169
|
-
segments: piecesThatMatter(track, finiteNonNegative(input.positionSeconds), horizonEnd) } : track);
|
|
170
|
+
Array.isArray(track.segments)
|
|
171
|
+
? { ...track, segments: piecesThatMatter(track, finiteNonNegative(input.positionSeconds), horizonEnd) } : track);
|
|
170
172
|
const duration = finiteNonNegative(input.durationSeconds);
|
|
171
173
|
// Each track's served ticks are placed on the clock the page reports by the
|
|
172
174
|
// offset its player applied (`timestampOffset`).
|
|
@@ -237,25 +239,30 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
237
239
|
.filter((segment) => isPiece(segment) &&
|
|
238
240
|
segment.endSeconds >= position - PLAYER_SEEK_TOLERANCE_SECONDS && segment.startSeconds < horizonEnd)
|
|
239
241
|
.sort((left, right) => left.startSeconds - right.startSeconds || left.index - right.index),
|
|
240
|
-
// The pieces after the near window, as the playlist cuts them, and the
|
|
241
|
-
// offset the page's player placed them by.
|
|
242
|
-
tail: [],
|
|
243
|
-
tailStartSeconds: horizonEnd,
|
|
244
242
|
durationSeconds: Number.isFinite(track.durationSeconds) ? track.durationSeconds : duration,
|
|
245
|
-
segmentCount: Number.isSafeInteger(track.segmentCount) ? track.segmentCount
|
|
246
|
-
: Math.max(0, ...track.segments.map(segment => segment.index + 1)),
|
|
247
243
|
clockSeconds: Number.isFinite(track.clockOffsetSeconds) ? track.clockOffsetSeconds : 0
|
|
248
244
|
}));
|
|
249
245
|
for (const state of trackState) {
|
|
250
|
-
state.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
state.trackEndSeconds = Math.min(duration, state.durationSeconds + state.clockSeconds);
|
|
247
|
+
}
|
|
248
|
+
// A file can declare a duration longer than its last packet. The HLS VOD
|
|
249
|
+
// playlist still has a final numbered piece, but its made fragment ends at
|
|
250
|
+
// the output's actual last frame. Waiting for the declaration then asks the
|
|
251
|
+
// source for media after EOF. The final piece and the browser's contiguous
|
|
252
|
+
// held range together are positive evidence of the terminal output: require
|
|
253
|
+
// both on every track the viewer must play.
|
|
254
|
+
const finishedOutputHeld = measured && trackState.slice(0, requiredTracks).every((state, index) => {
|
|
255
|
+
const count = Number(state.track.segmentCount);
|
|
256
|
+
if (!Number.isSafeInteger(count) || count <= 0 || !state.ready.has(count - 1)) return false;
|
|
257
|
+
const last = state.segments.find(segment => segment.index === count - 1);
|
|
258
|
+
if (!Array.isArray(last?.ranges) || last.ranges.length === 0) return false;
|
|
259
|
+
const heldEnd = contiguousEnd(measuredRanges[index], at);
|
|
260
|
+
if (!heldEnd) return false;
|
|
261
|
+
const outputEnd = latest(...last.ranges.map(range => range.end));
|
|
262
|
+
return compare(heldEnd, outputEnd) >= 0;
|
|
263
|
+
});
|
|
264
|
+
if (finishedOutputHeld) {
|
|
265
|
+
return result(true, 0, buffered, reserve, 0, "client-buffer-covers-end", 0);
|
|
259
266
|
}
|
|
260
267
|
if (trackState.some(({ segments }) => segments.length === 0)) {
|
|
261
268
|
return result(false, null, buffered, reserve, null, "timeline-unavailable", 0);
|
|
@@ -604,21 +611,12 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
604
611
|
// Supply interruption coverage is stock held by the proxy as well as
|
|
605
612
|
// the browser. It is not a browser buffer-size admission rule.
|
|
606
613
|
// The reserve is bounded by what is left of the track, not of the window.
|
|
607
|
-
const left = state.
|
|
608
|
-
: Math.max(0, state.tailEndSeconds - toSeconds(before));
|
|
614
|
+
const left = Math.max(0, state.trackEndSeconds - toSeconds(before));
|
|
609
615
|
const ahead = Math.max(0, secondsAhead(before, at) - Math.min(reserve, left) * Number(production.requiresSource));
|
|
610
616
|
constrain(production.at - ahead, "production", trackIndex, production.segment.index, production.at, ahead,
|
|
611
617
|
production.requiresSource, production.sourceArrivalAt, production.sourcePlanned);
|
|
612
618
|
}
|
|
613
619
|
}
|
|
614
|
-
if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
|
|
615
|
-
const stop = scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts, trackFinish,
|
|
616
|
-
link, position, buffered, reserve, transferFinish, constrain, duration });
|
|
617
|
-
if (stop) {
|
|
618
|
-
return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
|
|
619
|
-
...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
620
|
const firstRequiredStages = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
|
|
623
621
|
const first = state.segments.find(segment => segment.endSeconds > position);
|
|
624
622
|
if (!first) return { track: state.track.id ?? trackIndex, segment: null, alreadyBuffered: true,
|
|
@@ -683,93 +681,6 @@ function isPiece(segment) {
|
|
|
683
681
|
Number.isFinite(segment?.endSeconds) && segment.endSeconds > segment.startSeconds;
|
|
684
682
|
}
|
|
685
683
|
|
|
686
|
-
/**
|
|
687
|
-
* A duration-level estimate of the suffix after the exact near window. Source
|
|
688
|
-
* work uses each file's aggregate byte density and measured download service;
|
|
689
|
-
* processing uses each track's measured rate; delivery uses output bitrates and
|
|
690
|
-
* measured link service. Held source bytes and made segments reduce the
|
|
691
|
-
* estimate. This conditional forecast does not assert exact source ranges or
|
|
692
|
-
* individual frame timing. Its work is bounded by sources and tracks, not by
|
|
693
|
-
* the film's segment count.
|
|
694
|
-
*
|
|
695
|
-
* @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
|
|
696
|
-
*/
|
|
697
|
-
function scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts, trackFinish, link,
|
|
698
|
-
position, buffered, reserve, transferFinish, constrain, duration }) {
|
|
699
|
-
const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
|
|
700
|
-
if (!Number.isFinite(tailStart)) return null;
|
|
701
|
-
const sourceDurations = new Map();
|
|
702
|
-
for (const state of trackState) {
|
|
703
|
-
const seconds = Math.max(0, state.tailEndSeconds - tailStart) * state.tailWorkFraction;
|
|
704
|
-
if (!(seconds > 0)) continue;
|
|
705
|
-
for (const sourceId of new Set(state.track.sourceIds ?? [])) {
|
|
706
|
-
sourceDurations.set(sourceId, Math.max(sourceDurations.get(sourceId) ?? 0, seconds));
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
let sourceArrivalAt = 0;
|
|
711
|
-
for (const [sourceId, seconds] of sourceDurations) {
|
|
712
|
-
const source = sourceState.get(sourceId);
|
|
713
|
-
if (!source) return { reason: "source-measurement-unavailable" };
|
|
714
|
-
if (source.source.complete === true) continue;
|
|
715
|
-
const bytes = estimateUnmappedSourceBytes(source.source, seconds, duration,
|
|
716
|
-
sourceByteCoverage.get(sourceId));
|
|
717
|
-
if (bytes === null) return { reason: "source-measurement-unavailable",
|
|
718
|
-
unavailableSource: { sourceId, phase: "tail" } };
|
|
719
|
-
if (!(bytes > 0)) continue;
|
|
720
|
-
const { service } = source;
|
|
721
|
-
if (!service.rate || !(service.rate.rateAt(0) > 0)) {
|
|
722
|
-
return { reason: service.rate ? "service-not-advancing" : "download-rate-unavailable",
|
|
723
|
-
unavailableSource: { sourceId, estimatedBytes: bytes, phase: "tail" } };
|
|
724
|
-
}
|
|
725
|
-
service.finish = service.rate.finish(bytes, service.finish);
|
|
726
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
|
|
727
|
-
const state = trackState.find(candidate => candidate.track.sourceIds?.includes(sourceId));
|
|
728
|
-
const index = state?.segments.at(-1)?.index ?? 0;
|
|
729
|
-
const ahead = Math.max(0, (state?.tailEndSeconds ?? tailStart) - position);
|
|
730
|
-
constrain(service.finish - Math.max(0, ahead - reserve), "source", trackState.indexOf(state), index,
|
|
731
|
-
service.finish, ahead, true, service.finish, false);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
let outputBits = 0;
|
|
735
|
-
let latestTrackFinish = 0;
|
|
736
|
-
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
737
|
-
const state = trackState[trackIndex];
|
|
738
|
-
const end = state.tailEndSeconds;
|
|
739
|
-
const seconds = Math.max(0, end - tailStart);
|
|
740
|
-
if (!(seconds > 0)) continue;
|
|
741
|
-
const track = state.track;
|
|
742
|
-
const segment = state.segments.at(-1) ?? { index: 0 };
|
|
743
|
-
const remaining = seconds * state.tailWorkFraction;
|
|
744
|
-
const work = Math.max(0, remaining - numericCoverage((track.processingRanges ?? [])
|
|
745
|
-
.filter(range => Number.isFinite(range?.start) && Number.isFinite(range?.end))
|
|
746
|
-
.map(range => ({ start: Math.max(tailStart, range.start), end: Math.min(end, range.end) }))
|
|
747
|
-
.filter(range => range.end > range.start)).reduce((sum, range) => sum + range.end - range.start, 0));
|
|
748
|
-
const processing = state.curve;
|
|
749
|
-
if (work > 0 && !processing) return { reason: "encode-rate-unavailable" };
|
|
750
|
-
const run = runStarts[trackIndex];
|
|
751
|
-
const localStart = tailStart - state.clockSeconds;
|
|
752
|
-
const inRun = run.spans.some(span => localStart >= span.start && localStart < span.end);
|
|
753
|
-
const open = work > 0 && !inRun ? run.openSeconds : 0;
|
|
754
|
-
const finishedAt = work > 0 ? processing.finish(work, trackFinish[trackIndex] + open) : trackFinish[trackIndex];
|
|
755
|
-
latestTrackFinish = Math.max(latestTrackFinish, finishedAt);
|
|
756
|
-
const deliveryStart = Math.max(tailStart, position + buffered);
|
|
757
|
-
outputBits += Math.max(0, state.bitsPerMediaSecond) * Math.max(0, end - deliveryStart);
|
|
758
|
-
const ahead = Math.max(0, end - position);
|
|
759
|
-
constrain(finishedAt - Math.max(0, ahead - reserve), "production", trackIndex, segment.index,
|
|
760
|
-
finishedAt, ahead, sourceArrivalAt > 0, sourceArrivalAt, false);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (outputBits > 0) {
|
|
764
|
-
const end = Math.max(...trackState.map(state => state.tailEndSeconds));
|
|
765
|
-
const completedAt = link.finish(outputBits, Math.max(transferFinish, latestTrackFinish));
|
|
766
|
-
const ahead = Math.max(0, end - position);
|
|
767
|
-
constrain(completedAt - Math.max(0, ahead - reserve), "delivery", 0,
|
|
768
|
-
trackState[0]?.segments.at(-1)?.index ?? 0, completedAt, ahead, false, sourceArrivalAt, false);
|
|
769
|
-
}
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
684
|
/**
|
|
774
685
|
* Seconds from `from` to `to`, or zero when `to` is not later.
|
|
775
686
|
*
|
|
@@ -1075,7 +986,7 @@ function numericCoverage(ranges) {
|
|
|
1075
986
|
/**
|
|
1076
987
|
* Estimate download work without claiming an address or held byte. The whole
|
|
1077
988
|
* file's measured density, reduced by the held fraction, prices unresolved
|
|
1078
|
-
* intervals
|
|
989
|
+
* intervals in the near window.
|
|
1079
990
|
*/
|
|
1080
991
|
function estimateUnmappedSourceBytes(source, mediaSeconds, durationSeconds, coverage = null) {
|
|
1081
992
|
const fileLength = Number(source.fileLength);
|