@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
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
367
|
+
sourceService.finish = finish;
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
// Production accepts a complete held segment input. It cannot overlap its
|