@torrent-tv/proxy 2.105.3 → 2.105.5

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.105.3",
3
+ "version": "2.105.5",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -497,10 +497,11 @@ export class AviContainer extends Container {
497
497
  read.set(at, bytes);
498
498
  return bytes;
499
499
  };
500
- const chainAt = async start => {
500
+ const chainAt = async (start, firstOffset) => {
501
501
  let bytes = Buffer.alloc(0), cursor = 0, at = start, frames = 0;
502
502
  while (frames < 3 && at < stream.count && bytes.length < 32 * 1024) {
503
503
  bytes = Buffer.concat([bytes, await bytesAt(at++)]);
504
+ if (at === start + 1) cursor = firstOffset;
504
505
  while (bytes.length - cursor >= 7 && frames < 3) {
505
506
  if (bytes[cursor] !== 0x0b || bytes[cursor + 1] !== 0x77) return false;
506
507
  let frame;
@@ -515,14 +516,22 @@ export class AviContainer extends Container {
515
516
  };
516
517
 
517
518
  const anchor = stream.atOrBefore(interval.from);
518
- let first = -1, scannedBytes = 0;
519
+ let first = -1, firstOffset = -1, scannedBytes = 0;
519
520
  for (let at = anchor; at >= 0 && scannedBytes < 128 * 1024; at--) {
520
521
  const bytes = await bytesAt(at);
521
522
  scannedBytes += bytes.length;
522
- if (bytes.length >= 7 && bytes[0] === 0x0b && bytes[1] === 0x77 && await chainAt(at)) {
523
- first = at;
524
- break;
523
+ for (let offset = 0; offset < bytes.length; offset++) {
524
+ if (bytes[offset] !== 0x0b) continue;
525
+ if (offset + 1 < bytes.length) {
526
+ if (bytes[offset + 1] !== 0x77) continue;
527
+ } else if (at + 1 >= stream.count || (await bytesAt(at + 1))[0] !== 0x77) continue;
528
+ if (await chainAt(at, offset)) {
529
+ first = at;
530
+ firstOffset = offset;
531
+ break;
532
+ }
525
533
  }
534
+ if (first >= 0) break;
526
535
  }
527
536
  if (first < 0) throw new Error("Indexed AVI AC-3 has no complete frame chain near the requested interval.");
528
537
 
@@ -530,7 +539,9 @@ export class AviContainer extends Container {
530
539
  try {
531
540
  for (let at = first; at < stream.count; at++) {
532
541
  const bytes = await bytesAt(at);
533
- if (bytes.length) elementary.push(track.trackNumber, bytes, stream.starts[at], at === first ? { pts: stream.times[first] } : {});
542
+ const offset = at === first ? firstOffset : 0;
543
+ if (bytes.length > offset) elementary.push(track.trackNumber, bytes.subarray(offset), stream.starts[at] + offset,
544
+ at === first ? { pts: stream.times[first] } : {});
534
545
  const input = elementary.index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to,
535
546
  mode: interval.modes?.[track.trackNumber] ?? "transcode" });
536
547
  if (input.kind === "result") {
@@ -147,7 +147,8 @@ export class ViewerRequests {
147
147
  const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
148
148
  if (!Number.isFinite(seconds) || seconds < 0) return null;
149
149
  this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
150
- firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
150
+ firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds,
151
+ firstRequiredStages: context.activeFirstRequiredStages });
151
152
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
152
153
  }
153
154
 
@@ -192,13 +193,19 @@ export class ViewerRequests {
192
193
  const elapsedMs = waitElapsedSince(context, waitElapsedMs);
193
194
  const measuredWorkSeconds = processing.firstRequiredSegmentsSeconds;
194
195
  const hasActiveEstimate = Number.isFinite(measuredWorkSeconds) && measuredWorkSeconds >= 0;
196
+ const stagesChanged = Array.isArray(processing.firstRequiredStages) &&
197
+ JSON.stringify(processing.firstRequiredStages) !== JSON.stringify(context.activeFirstRequiredStages);
195
198
  const activeTotalSeconds = hasActiveEstimate
196
199
  ? elapsedMs / 1000 + measuredWorkSeconds
197
200
  : context.activeTotalSeconds;
198
- const activeEstimateChanged = hasActiveEstimate && activeTotalSeconds !== context.activeTotalSeconds;
201
+ const activeEstimateChanged = hasActiveEstimate &&
202
+ (activeTotalSeconds !== context.activeTotalSeconds || stagesChanged);
199
203
  if (hasActiveEstimate) {
200
204
  context.activeTotalSeconds = activeTotalSeconds;
201
205
  context.activeFirstRequiredSegmentsSeconds = measuredWorkSeconds;
206
+ if (Array.isArray(processing.firstRequiredStages)) {
207
+ context.activeFirstRequiredStages = processing.firstRequiredStages;
208
+ }
202
209
  }
203
210
  const changed = featuresChanged || activeEstimateChanged;
204
211
  if (sameWait && changed) context.revision += 1;
@@ -213,7 +220,8 @@ export class ViewerRequests {
213
220
  const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
214
221
  if (!Number.isFinite(seconds) || seconds < 0) return null;
215
222
  this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
216
- firstRequiredSegmentsSeconds: hasActiveEstimate ? measuredWorkSeconds : context.activeFirstRequiredSegmentsSeconds });
223
+ firstRequiredSegmentsSeconds: hasActiveEstimate ? measuredWorkSeconds : context.activeFirstRequiredSegmentsSeconds,
224
+ firstRequiredStages: context.activeFirstRequiredStages });
217
225
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
218
226
  }
219
227
 
@@ -222,11 +230,12 @@ export class ViewerRequests {
222
230
  return Number.isFinite(estimate) ? { seconds: estimate, stagesMs: null, observations: null } : estimate;
223
231
  }
224
232
 
225
- #logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical, firstRequiredSegmentsSeconds }) {
233
+ #logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical, firstRequiredSegmentsSeconds,
234
+ firstRequiredStages }) {
226
235
  if (context.loggedForecastRevision === context.revision) return;
227
236
  const basis = Number.isFinite(firstRequiredSegmentsSeconds) ? "first-required" : "historical-stages";
228
237
  const estimate = basis === "first-required"
229
- ? { firstRequiredSegmentsSeconds }
238
+ ? { firstRequiredSegmentsSeconds, ...(Array.isArray(firstRequiredStages) ? { firstRequiredStages } : {}) }
230
239
  : { stagesMs: historical?.stagesMs ?? null, observations: historical?.observations ?? null };
231
240
  logger.info(`early wait forecast ${JSON.stringify({ waitId: context.waitId, consumer: context.consumerId,
232
241
  generation: context.generation, features: context.features, elapsedMs, revision: context.revision,
@@ -1062,7 +1071,8 @@ export class ViewerRequests {
1062
1071
  audioOperation: audioOperation?.operation,
1063
1072
  videoSpeedX: videoOperation?.speed,
1064
1073
  audioSpeedX: audioOperation?.speed,
1065
- firstRequiredSegmentsSeconds: forecast.firstRequiredSegmentsSeconds
1074
+ firstRequiredSegmentsSeconds: forecast.firstRequiredSegmentsSeconds,
1075
+ firstRequiredStages: forecast.firstRequiredStages
1066
1076
  } });
1067
1077
  if (waitEstimate) forecast.waitEstimate = waitEstimate;
1068
1078
  }
@@ -1105,6 +1115,7 @@ export class ViewerRequests {
1105
1115
  const holding = forecast.holdingBy;
1106
1116
  this.#host.noteViewerWaiting?.(consumerId || session.id,
1107
1117
  holding?.kind === "production" ? { address: holding.track, index: holding.segment } : null);
1118
+ delete forecast.firstRequiredStages;
1108
1119
  return forecast;
1109
1120
  }
1110
1121
 
@@ -463,23 +463,28 @@ export function predictPlaybackReadiness(input = {}) {
463
463
  // Production accepts a complete held segment input. It cannot overlap its
464
464
  // own source download; reused input retains its original arrival time.
465
465
  const processing = trackState[trackIndex].curve;
466
- let admittedAt = Math.max(trackFinish[trackIndex], sourceArrivalAt);
466
+ let runAdmissionAt = null;
467
+ let runOpenSeconds = 0;
468
+ let productionStartAt = Math.max(trackFinish[trackIndex], sourceArrivalAt);
467
469
  if (!job.produced && encodeWork > 0) {
468
470
  const run = runStarts[trackIndex];
469
471
  const live = run.spans.some(span => segment.startSeconds >= span.start && segment.startSeconds < span.end);
470
472
  if (!live && (run.admittedAt === null || sourceArrivalAt > run.admittedAt)) {
471
- run.admittedAt = admittedAt;
472
- admittedAt += run.openSeconds;
473
+ runAdmissionAt = productionStartAt;
474
+ run.admittedAt = runAdmissionAt;
475
+ runOpenSeconds = run.openSeconds;
476
+ productionStartAt += runOpenSeconds;
473
477
  }
474
478
  }
475
- const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, admittedAt);
479
+ const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, productionStartAt);
476
480
  trackFinish[trackIndex] = Math.max(trackFinish[trackIndex], producedAt);
477
481
  const sourcePlanned = sourceArrivalAt > 0 && plan.finish >= sourceArrivalAt;
478
482
  productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivalAt > 0, sourceArrivalAt,
479
- sourcePlanned });
483
+ sourcePlanned, runAdmissionAt, runOpenSeconds, productionStartAt, encodeWork });
480
484
  const deliveryAt = link.finish(sizeBits, Math.max(transferFinish, producedAt));
481
485
  transferFinish = deliveryAt;
482
- completions.push({ at: deliveryAt + finiteNonNegative(track.appendSeconds), trackIndex, segment, sourceArrivalAt,
486
+ const appendSeconds = finiteNonNegative(track.appendSeconds);
487
+ completions.push({ at: deliveryAt + appendSeconds, deliveryAt, appendSeconds, trackIndex, segment, sourceArrivalAt,
483
488
  sourcePlanned });
484
489
  }
485
490
 
@@ -547,13 +552,32 @@ export function predictPlaybackReadiness(input = {}) {
547
552
  ...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
548
553
  }
549
554
  }
550
- const firstRequiredTrackSeconds = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
555
+ const firstRequiredStages = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
551
556
  const first = state.segments.find(segment => segment.endSeconds > position);
552
- if (!first) return 0;
553
- if (first.ranges.every(range => contains(state.clientRanges, range.start, range.end))) return 0;
557
+ if (!first) return { track: state.track.id ?? trackIndex, segment: null, alreadyBuffered: true,
558
+ sourceArrivalSeconds: 0, runAdmissionSeconds: null, runOpenSeconds: 0, productionStartSeconds: 0,
559
+ productionCompleteSeconds: 0, deliveryCompleteSeconds: 0, appendSeconds: 0, completionSeconds: 0 };
560
+ if (first.ranges.every(range => contains(state.clientRanges, range.start, range.end))) {
561
+ return { track: state.track.id ?? trackIndex, segment: first.index, alreadyBuffered: true,
562
+ sourceArrivalSeconds: 0, runAdmissionSeconds: null, runOpenSeconds: 0, productionStartSeconds: 0,
563
+ productionCompleteSeconds: 0, deliveryCompleteSeconds: 0, appendSeconds: 0, completionSeconds: 0 };
564
+ }
554
565
  const completion = completions.find(item => item.trackIndex === trackIndex && item.segment.index === first.index);
555
- return completion?.at ?? null;
566
+ if (!completion) return { track: state.track.id ?? trackIndex, segment: first.index, alreadyBuffered: false,
567
+ sourceArrivalSeconds: null, runAdmissionSeconds: null, runOpenSeconds: null, productionStartSeconds: null,
568
+ productionCompleteSeconds: null, deliveryCompleteSeconds: null, appendSeconds: null, completionSeconds: null };
569
+ const production = productions.find(item => item.trackIndex === trackIndex && item.segment.index === first.index);
570
+ return { track: state.track.id ?? trackIndex, segment: first.index, alreadyBuffered: false,
571
+ sourceArrivalSeconds: production?.sourceArrivalAt ?? 0,
572
+ runAdmissionSeconds: production?.runAdmissionAt ?? null,
573
+ runOpenSeconds: production?.runOpenSeconds ?? 0,
574
+ productionStartSeconds: production?.productionStartAt ?? null,
575
+ productionCompleteSeconds: production?.at ?? null,
576
+ deliveryCompleteSeconds: completion.deliveryAt,
577
+ appendSeconds: completion.appendSeconds,
578
+ completionSeconds: completion.at };
556
579
  });
580
+ const firstRequiredTrackSeconds = firstRequiredStages.map(stage => stage.completionSeconds);
557
581
  const firstRequiredSegmentsSeconds = firstRequiredTrackSeconds.every(Number.isFinite)
558
582
  ? Math.max(0, ...firstRequiredTrackSeconds) : null;
559
583
  const delay = Math.max(0, ...constraints);
@@ -564,6 +588,7 @@ export function predictPlaybackReadiness(input = {}) {
564
588
  Number.isFinite(delay) ? 'minimum-safe-delay' : 'service-not-advancing', preparedSegments);
565
589
  return { ...forecast, measuredAt: now, predictedStartAt: Number.isFinite(delay) ? now + delay * 1000 : null,
566
590
  firstRequiredSegmentsSeconds,
591
+ firstRequiredStages,
567
592
  // The piece whose `kind` — its production, or its delivery to the page —
568
593
  // comes `atSeconds` from now while the media before it lasts
569
594
  // `aheadSeconds`: the difference is the delay.