@torrent-tv/proxy 2.105.2 → 2.105.4
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
|
@@ -18,6 +18,7 @@ import { readAviIndex } from "./avi-index.js";
|
|
|
18
18
|
import { isUnavailable } from "./unavailable.js";
|
|
19
19
|
import { Mpeg4PictureTiming } from "./mpeg4-picture-timing.js";
|
|
20
20
|
import { MpegElementaryIndex } from "./mpeg-elementary-index.js";
|
|
21
|
+
import { ac3Frame } from "./ac3-frame.js";
|
|
21
22
|
import { RetainedReads } from "./RetainedReads.js";
|
|
22
23
|
import { OutsideReadableEdges, edgeReader, emptyWorkTags, text, textList, yearOf } from "./work-tags.js";
|
|
23
24
|
|
|
@@ -208,8 +209,14 @@ export class AviContainer extends Container {
|
|
|
208
209
|
* ones, and the swarm was asked for the picture's next piece only after it
|
|
209
210
|
* (torrent-tv/meta#178).
|
|
210
211
|
*/
|
|
211
|
-
packetTableWouldWait() {
|
|
212
|
+
packetTableWouldWait(interval = null) {
|
|
212
213
|
if (!this.#index || this.#packets || this.#scan) return false;
|
|
214
|
+
if (interval?.trackIds?.length) {
|
|
215
|
+
const tracks = this.#tracks ?? [];
|
|
216
|
+
const selected = tracks.filter(track => interval.trackIds.includes(track.trackNumber));
|
|
217
|
+
if (selected.length === interval.trackIds.length && selected.length === 1 && selected[0].type === "audio" &&
|
|
218
|
+
selected[0].codecId === "ac3") return false;
|
|
219
|
+
}
|
|
213
220
|
return this.#soundPacketsRefused === null || this.packetMemory?.fits?.(this.#soundPacketsRefused) !== false;
|
|
214
221
|
}
|
|
215
222
|
|
|
@@ -414,7 +421,16 @@ export class AviContainer extends Container {
|
|
|
414
421
|
}
|
|
415
422
|
|
|
416
423
|
async readPacketIndex(interval) {
|
|
417
|
-
if (this.#packets)
|
|
424
|
+
if (this.#packets) {
|
|
425
|
+
if (this.#index && interval?.trackIds?.length) {
|
|
426
|
+
const tracks = await this.readTracks();
|
|
427
|
+
const selected = tracks.filter(track => interval.trackIds.includes(track.trackNumber));
|
|
428
|
+
if (selected.length === 1 && selected[0].type === "audio" && selected[0].codecId === "ac3") {
|
|
429
|
+
return this.#indexedAc3PacketsOf(this.#index, selected[0], interval);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return this.#packets;
|
|
433
|
+
}
|
|
418
434
|
if (this.#scan) return this.#readUnindexed(interval);
|
|
419
435
|
// A picture of a file with an index is read by FFmpeg from the original
|
|
420
436
|
// bytes (torrent-tv/meta#151). Its SOUND is not: the index states every
|
|
@@ -424,6 +440,12 @@ export class AviContainer extends Container {
|
|
|
424
440
|
// copied 2.3 GB for 0.19 GB of its own (torrent-tv/meta#166).
|
|
425
441
|
const indexed = await this.#readIndex();
|
|
426
442
|
if (indexed) {
|
|
443
|
+
const tracks = await this.readTracks();
|
|
444
|
+
const selected = interval?.trackIds?.length
|
|
445
|
+
? tracks.filter(track => interval.trackIds.includes(track.trackNumber)) : [];
|
|
446
|
+
if (selected.length === 1 && selected[0].type === "audio" && selected[0].codecId === "ac3") {
|
|
447
|
+
return this.#indexedAc3PacketsOf(indexed, selected[0], interval);
|
|
448
|
+
}
|
|
427
449
|
// A refusal of memory for the table is kept for as long as the memory's
|
|
428
450
|
// owner says the same reservation still would not fit. Not kept, the
|
|
429
451
|
// next reader walked the index, counted the packets and was refused
|
|
@@ -463,6 +485,68 @@ export class AviContainer extends Container {
|
|
|
463
485
|
return this.#readUnindexed(interval);
|
|
464
486
|
}
|
|
465
487
|
|
|
488
|
+
/** Parse only the indexed AC-3 blocks needed for one audio interval. */
|
|
489
|
+
async #indexedAc3PacketsOf(index, track, interval) {
|
|
490
|
+
const stream = index.packets[track.trackNumber];
|
|
491
|
+
if (!stream?.count) throw new Error("Indexed AVI AC-3 track has no packets.");
|
|
492
|
+
const read = new Map();
|
|
493
|
+
const bytesAt = async at => {
|
|
494
|
+
if (read.has(at)) return read.get(at);
|
|
495
|
+
const length = stream.lengths[at];
|
|
496
|
+
const bytes = length ? await this.readRange(stream.starts[at], stream.starts[at] + length - 1) : Buffer.alloc(0);
|
|
497
|
+
read.set(at, bytes);
|
|
498
|
+
return bytes;
|
|
499
|
+
};
|
|
500
|
+
const chainAt = async start => {
|
|
501
|
+
let bytes = Buffer.alloc(0), cursor = 0, at = start, frames = 0;
|
|
502
|
+
while (frames < 3 && at < stream.count && bytes.length < 32 * 1024) {
|
|
503
|
+
bytes = Buffer.concat([bytes, await bytesAt(at++)]);
|
|
504
|
+
while (bytes.length - cursor >= 7 && frames < 3) {
|
|
505
|
+
if (bytes[cursor] !== 0x0b || bytes[cursor + 1] !== 0x77) return false;
|
|
506
|
+
let frame;
|
|
507
|
+
try { frame = ac3Frame(bytes.subarray(cursor)); }
|
|
508
|
+
catch { return false; }
|
|
509
|
+
if (bytes.length - cursor < frame.size) break;
|
|
510
|
+
cursor += frame.size;
|
|
511
|
+
frames++;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return frames === 3;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const anchor = stream.atOrBefore(interval.from);
|
|
518
|
+
let first = -1, scannedBytes = 0;
|
|
519
|
+
for (let at = anchor; at >= 0 && scannedBytes < 128 * 1024; at--) {
|
|
520
|
+
const bytes = await bytesAt(at);
|
|
521
|
+
scannedBytes += bytes.length;
|
|
522
|
+
if (bytes.length >= 7 && bytes[0] === 0x0b && bytes[1] === 0x77 && await chainAt(at)) {
|
|
523
|
+
first = at;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (first < 0) throw new Error("Indexed AVI AC-3 has no complete frame chain near the requested interval.");
|
|
528
|
+
|
|
529
|
+
const elementary = new MpegElementaryIndex([track], { packetMemory: this.packetMemory });
|
|
530
|
+
try {
|
|
531
|
+
for (let at = first; at < stream.count; at++) {
|
|
532
|
+
const bytes = await bytesAt(at);
|
|
533
|
+
if (bytes.length) elementary.push(track.trackNumber, bytes, stream.starts[at], at === first ? { pts: stream.times[first] } : {});
|
|
534
|
+
const input = elementary.index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to,
|
|
535
|
+
mode: interval.modes?.[track.trackNumber] ?? "transcode" });
|
|
536
|
+
if (input.kind === "result") {
|
|
537
|
+
elementary.index.disposeAfterUse = true;
|
|
538
|
+
return elementary.index;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
elementary.complete();
|
|
542
|
+
elementary.index.disposeAfterUse = true;
|
|
543
|
+
return elementary.index;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
elementary.index.dispose();
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
466
550
|
/**
|
|
467
551
|
* The sound packets an AVI index states, on FFmpeg's clock: the time the
|
|
468
552
|
* index gives each packet is the one FFmpeg's demuxer gives it, and a packet
|
|
@@ -33,11 +33,15 @@ export async function directSegmentInput(container, { interval, tracks, mode },
|
|
|
33
33
|
const walked = await container.packetWalkRanges?.(interval);
|
|
34
34
|
if (walked) return walked;
|
|
35
35
|
// The table is begun, or joined, and not waited for.
|
|
36
|
-
if (container.packetTableWouldWait?.() === true) {
|
|
36
|
+
if (container.packetTableWouldWait?.(interval) === true) {
|
|
37
37
|
whenBuilt(container.readPacketIndex(interval).then(() => undefined, () => undefined));
|
|
38
38
|
return { kind: "pending" };
|
|
39
39
|
}
|
|
40
40
|
const index = await container.readPacketIndex(interval);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
try {
|
|
42
|
+
await index.prepareAudioDependencies?.(interval, container.readRange);
|
|
43
|
+
return new SegmentInputs({ index, tracks }).forInterval({ ...interval, mode });
|
|
44
|
+
} finally {
|
|
45
|
+
if (index.disposeAfterUse === true) index.dispose();
|
|
46
|
+
}
|
|
43
47
|
}
|
|
@@ -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 &&
|
|
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
|
|
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
|
-
|
|
472
|
-
admittedAt
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
553
|
-
|
|
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
|
-
|
|
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.
|