@torrent-tv/proxy 2.95.31 → 2.95.33

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.95.31",
3
+ "version": "2.95.33",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -129,6 +129,8 @@ export function buildRunCommand({
129
129
  args.push("-i", audioInputUrl);
130
130
  };
131
131
 
132
+ // Whether an output `-ss` already cuts at the interval's start.
133
+ let trimmedAtStart = false;
132
134
  if (snappedKeyframe !== null) {
133
135
  const residualSeconds = Math.max(0, seekAt - snappedKeyframe);
134
136
  if (snappedKeyframe > sourceStartTime) {
@@ -138,6 +140,7 @@ export function buildRunCommand({
138
140
  pushAudioInput(snappedKeyframe);
139
141
  if (residualSeconds > 0 && !keyframeGrid) {
140
142
  args.push("-ss", ffmpegSeconds(residualSeconds));
143
+ trimmedAtStart = true;
141
144
  }
142
145
  } else {
143
146
  if (seekAt > sourceStartTime) {
@@ -146,6 +149,15 @@ export function buildRunCommand({
146
149
  args.push("-i", inputUrl);
147
150
  pushAudioInput(seekAt);
148
151
  }
152
+ // An input seek lands on the picture's keyframe at or before the time asked
153
+ // for, in every stream (AVI `avi_read_seek`, Matroska Cues). `-accurate_seek`
154
+ // trims only what is decoded, so a copied soundtrack began at that keyframe
155
+ // and `-t` counted its length from there: a piece of a LostFilm AVI with a
156
+ // keyframe every 11 s carried the film's first four seconds (torrent-tv/meta#159).
157
+ // Without `-copyts` the time asked for is zero on the output's input clock.
158
+ if (!keyframeGrid && !servesAudioSeparately && !transcodeAudio && !trimmedAtStart) {
159
+ args.push("-ss", "0");
160
+ }
149
161
  if (!keyframeGrid) {
150
162
  if (startSeconds > 0) {
151
163
  args.push("-output_ts_offset", ffmpegSeconds(startSeconds));
@@ -232,8 +232,13 @@ export class AviContainer extends Container {
232
232
  } else if (type === "audio") {
233
233
  if (strf.length < 16) throw new Error("AVI wave format is truncated.");
234
234
  const tag = strf.readUInt16LE(0);
235
+ // WAVEFORMATEX states the average bytes per second the stream carries
236
+ // (`nAvgBytesPerSec`); zero states nothing. Without it a soundtrack the
237
+ // browser plays is re-encoded only because its copy has no figure to
238
+ // ask the link about (torrent-tv/meta#159).
235
239
  track = new AudioTrack({ ...params, codecId: waveCodec(tag),
236
240
  channels: strf.readUInt16LE(2), samplingFrequency: strf.readUInt32LE(4),
241
+ bitrateKbps: strf.readUInt32LE(8) * 8 / 1000,
237
242
  codecPrivateB64: strf.subarray(18).toString("base64") });
238
243
  track.blockAlign = strf.readUInt16LE(12);
239
244
  track.matroskaCodecId = "A_MS/ACM";
@@ -78,6 +78,8 @@ export class AudioTrack extends ContainerTrack {
78
78
  * @param {boolean} params.isVisualImpaired - FlagVisualImpaired / descriptive audio
79
79
  * @param {number | null} params.channels
80
80
  * @param {number | null} params.samplingFrequency
81
+ * @param {number | null} [params.bitrateKbps] - The rate the file states for
82
+ * this track, in kbit/s: an average, not a bound.
81
83
  */
82
84
  constructor(params) {
83
85
  super({ ...params, type: "audio" });
@@ -86,6 +88,7 @@ export class AudioTrack extends ContainerTrack {
86
88
  this.isVisualImpaired = params.isVisualImpaired === true;
87
89
  this.channels = Number.isFinite(params.channels) ? params.channels : null;
88
90
  this.samplingFrequency = Number.isFinite(params.samplingFrequency) ? params.samplingFrequency : null;
91
+ this.bitrateKbps = Number.isFinite(params.bitrateKbps) && params.bitrateKbps > 0 ? params.bitrateKbps : null;
89
92
  this.bitDepth = Number.isSafeInteger(params.bitDepth) && params.bitDepth > 0 ? params.bitDepth : null;
90
93
  this.codecDelaySeconds = Number.isFinite(params.codecDelaySeconds) && params.codecDelaySeconds >= 0 ? params.codecDelaySeconds : 0;
91
94
  // What the codec is configured as, and the most that configuration allows
@@ -612,12 +612,13 @@ export class ViewerRequests {
612
612
  })).filter(range => range.end > range.start) } : sharedForecast;
613
613
  sourceMeasurements.set(sourceId, {
614
614
  id: sourceId,
615
+ serviceKey: output.file.sourceKey,
615
616
  complete: stats?.fileAvailable === true,
616
617
  residence: stats?.residence,
617
618
  fileOffset: stats?.fileOffset,
618
619
  fileLength: stats?.fileLength,
619
620
  pieceLength: stats?.pieceLength,
620
- downloadRateReadings: this.#sourceRateReadings(measurement, sourceId, stats, now),
621
+ downloadRateReadings: this.#sourceRateReadings(measurement, output.file.sourceKey, stats, now),
621
622
  downloadForecast,
622
623
  });
623
624
  }
@@ -99,7 +99,8 @@ export class RateTrend {
99
99
  * @param {number} input.lookaheadSeconds
100
100
  * @param {number} input.now
101
101
  * @param {boolean} [input.requiredAudio]
102
- * @param {Array<{ id: string, complete: boolean,
102
+ * @param {Array<{ id: string, serviceKey?: string, complete: boolean, fileOffset?: number,
103
+ * fileLength?: number, pieceLength?: number, downloadRateReadings?: object,
103
104
  * downloadForecast?: { ranges: Array<{ start: number, end: number, availableAt: number | null }> } }>} input.sources
104
105
  * @param {Array<{ id: string, sourceIds: string[], processedSeconds: number,
105
106
  * bitsPerMediaSecond: number, readings: Array<{ at: number, value: number }>,
@@ -192,8 +193,16 @@ export function predictPlaybackReadiness(input = {}) {
192
193
  return result(false, null, buffered, reserve, null, "timeline-unavailable", 0);
193
194
  }
194
195
  const sourceState = new Map();
196
+ // A torrent has one download service, even when required media lives in separate files.
197
+ const sourceServices = new Map();
195
198
  for (const [id, source] of sources) {
196
- sourceState.set(id, { source, rate: rateCurve(source.downloadRateReadings, now), serviceFinish: 0 });
199
+ const serviceKey = typeof source.serviceKey === "string" ? source.serviceKey : id;
200
+ let service = sourceServices.get(serviceKey);
201
+ if (!service) {
202
+ service = { rate: rateCurve(source.downloadRateReadings, now), finish: 0 };
203
+ sourceServices.set(serviceKey, service);
204
+ }
205
+ sourceState.set(id, { source, service });
197
206
  }
198
207
  const link = rateCurve(input.linkReadings, now);
199
208
  let unknownReason = null;
@@ -333,7 +342,8 @@ export function predictPlaybackReadiness(input = {}) {
333
342
  // pieces, project only the media-order demand from the torrent's latest
334
343
  // measured download service; no supplier or positive rate means no
335
344
  // estimated arrival.
336
- const { source: facts, rate } = source;
345
+ const { source: facts, service: sourceService } = source;
346
+ const { rate } = sourceService;
337
347
  if (!rate || !(rate.rateAt(0) > 0) || !Number.isSafeInteger(facts.pieceLength) ||
338
348
  facts.pieceLength <= 0 || !Number.isSafeInteger(facts.fileOffset) ||
339
349
  !Number.isSafeInteger(facts.fileLength) || facts.fileLength <= 0) {
@@ -342,7 +352,7 @@ export function predictPlaybackReadiness(input = {}) {
342
352
  unavailableSource: { sourceId, segmentIndex: segment.index, range: interval, missing } };
343
353
  }
344
354
  const estimatedPieces = sourcePiecesFor(missing, facts);
345
- let finish = Math.max(source.serviceFinish, sourceArrivalAt);
355
+ let finish = Math.max(sourceService.finish, sourceArrivalAt);
346
356
  const service = sourceByteService.get(sourceId) ?? [];
347
357
  for (const pieceRange of estimatedPieces) {
348
358
  const stillMissing = uncoveredIntervals(pieceRange, sourceByteCoverage.get(sourceId) ?? []);
@@ -354,7 +364,7 @@ export function predictPlaybackReadiness(input = {}) {
354
364
  sourceByteCoverage.set(sourceId, numericCoverage(service));
355
365
  sourceArrivals.set(sourceId, indexedArrivals(service));
356
366
  }
357
- source.serviceFinish = finish;
367
+ sourceService.finish = finish;
358
368
  }
359
369
  }
360
370
  // Production accepts a complete held segment input. It cannot overlap its