@torrent-tv/proxy 2.94.0 → 2.94.2
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 +1 -1
- package/services/media/container/AviContainer.js +1 -1
- package/services/media/container/Container.js +19 -1
- package/services/media/container/MatroskaContainer.js +1 -1
- package/services/media/container/Mp4Container.js +2 -2
- package/services/server/ViewerRequests.js +8 -1
- package/services/server/subtitle-readiness.js +38 -0
- package/services/server/wire-outputs.js +3 -0
- package/services/viewer/SourcePreparation.js +14 -2
package/package.json
CHANGED
|
@@ -102,7 +102,7 @@ export class AviContainer extends Container {
|
|
|
102
102
|
if (!(error instanceof OutsideReadableEdges)) throw error;
|
|
103
103
|
tags.outsideEdges = true;
|
|
104
104
|
}
|
|
105
|
-
tags.trackTitles =
|
|
105
|
+
tags.trackTitles = await this.trackTitlesForWork();
|
|
106
106
|
if (!tags.outsideEdges) this.#workTags = tags;
|
|
107
107
|
return tags;
|
|
108
108
|
}
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* - AVI RIFF §: LIST hdrl, idx1
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { strictReader } from "./unavailable.js";
|
|
15
|
+
import { isUnavailable, strictReader } from "./unavailable.js";
|
|
16
|
+
import { textList } from "./work-tags.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* What one file declares about itself. Every field is either a value the
|
|
@@ -514,6 +515,23 @@ export class Container {
|
|
|
514
515
|
return null;
|
|
515
516
|
}
|
|
516
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The titles of the file's tracks, for what it states about its work. A
|
|
520
|
+
* track table this program refuses to read costs only the titles: the
|
|
521
|
+
* statements about the work are read from other elements. Bytes that have
|
|
522
|
+
* not arrived are still "not here yet".
|
|
523
|
+
*
|
|
524
|
+
* @returns {Promise<string[]>}
|
|
525
|
+
*/
|
|
526
|
+
async trackTitlesForWork() {
|
|
527
|
+
try {
|
|
528
|
+
return textList((await this.readTracks()).map((track) => track.name));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (isUnavailable(error)) throw error;
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
517
535
|
/**
|
|
518
536
|
* The cover image the file carries, checked to be an image by its bytes.
|
|
519
537
|
* `null` where it carries none.
|
|
@@ -437,7 +437,7 @@ export class MatroskaContainer extends Container {
|
|
|
437
437
|
const cover = await this.#coverAddress(reader, layout);
|
|
438
438
|
if (cover && cover.size > 0 && cover.size <= MAX_COVER_BYTES) tags.cover = { type: cover.mediaType, size: cover.size };
|
|
439
439
|
} catch (error) { leftOut(error); }
|
|
440
|
-
tags.trackTitles =
|
|
440
|
+
tags.trackTitles = await this.trackTitlesForWork();
|
|
441
441
|
if (!tags.outsideEdges) this.#workTags = tags;
|
|
442
442
|
return tags;
|
|
443
443
|
}
|
|
@@ -28,7 +28,7 @@ import { hevcConfiguration } from "./hevc-configuration.js";
|
|
|
28
28
|
import { mpegAudioFrame } from "./mpeg-audio-frame.js";
|
|
29
29
|
import { readMp4Fragments } from "./mp4-fragments.js";
|
|
30
30
|
import { workFromMoov } from "./mp4-work-tags.js";
|
|
31
|
-
import { COVER_TYPES, MAX_COVER_BYTES, emptyWorkTags, imageTypeOf
|
|
31
|
+
import { COVER_TYPES, MAX_COVER_BYTES, emptyWorkTags, imageTypeOf } from "./work-tags.js";
|
|
32
32
|
|
|
33
33
|
/** A box header is eight bytes, or sixteen when the size field says 1 (§4.2). */
|
|
34
34
|
const HEADER_BYTES = 8;
|
|
@@ -532,7 +532,7 @@ export class Mp4Container extends Container {
|
|
|
532
532
|
Object.assign(tags, work);
|
|
533
533
|
if (cover && cover.size > 0 && cover.size <= MAX_COVER_BYTES) tags.cover = { type: cover.type, size: cover.size };
|
|
534
534
|
}
|
|
535
|
-
tags.trackTitles =
|
|
535
|
+
tags.trackTitles = await this.trackTitlesForWork();
|
|
536
536
|
this.workTags = tags;
|
|
537
537
|
return tags;
|
|
538
538
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
11
11
|
import { isOutputName } from "../encode/output/index.js";
|
|
12
|
+
import { withSubtitleReadiness } from "./subtitle-readiness.js";
|
|
12
13
|
|
|
13
14
|
function isWarmupTimeoutError(error) {
|
|
14
15
|
if (!(error instanceof Error)) {
|
|
@@ -696,7 +697,13 @@ export class ViewerRequests {
|
|
|
696
697
|
tracks,
|
|
697
698
|
linkReadings: measurement.link.snapshot()
|
|
698
699
|
};
|
|
699
|
-
|
|
700
|
+
// A subtitle track chosen inside THIS file is the last input the forecast
|
|
701
|
+
// waits for. A subtitle FILE beside it is named by its own index and is
|
|
702
|
+
// fetched whole by the page before it is chosen, so it never matches here.
|
|
703
|
+
const subtitle = viewer?.subtitle?.sourceKey === session.file.sourceKey &&
|
|
704
|
+
viewer.subtitle.fileIndex === session.file.fileIndex ? viewer.subtitle : null;
|
|
705
|
+
const forecast = withSubtitleReadiness(this.#host.playbackReadiness.predict(input), subtitle,
|
|
706
|
+
subtitle ? this.#host.subtitleReadyFor?.(viewer) !== false : true);
|
|
700
707
|
forecast.operations = outputs.flatMap((output, index) => {
|
|
701
708
|
const common = { speed: this.#host.playbackReadiness.forecastRate(tracks[index].readings), processedSeconds: tracks[index].processedSeconds };
|
|
702
709
|
return [
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The subtitle a viewer has chosen, as a term of their playback readiness.
|
|
3
|
+
*
|
|
4
|
+
* A file that says which subtitle to show — `FlagDefault` — is saying the film
|
|
5
|
+
* is to be watched with it, the opening included. Starting the picture before
|
|
6
|
+
* that track's cues have been read where the viewer stands showed the opening
|
|
7
|
+
* without them (torrent-tv/meta#8). So the chosen track is one more input the
|
|
8
|
+
* forecast waits for, beside the picture and the separately published sound.
|
|
9
|
+
*
|
|
10
|
+
* **What bounds the wait.** The read it waits for is one segment of that track
|
|
11
|
+
* at the viewer's position (`SourcePreparation`, the `subtitle-embedded`
|
|
12
|
+
* work), demanded at the picture's own urgency. It ends with the cues or with
|
|
13
|
+
* the proxy's refusal to read the track, and `subtitleReadyFor` counts both.
|
|
14
|
+
* No time limit is added: there is no measured figure to set one from, and the
|
|
15
|
+
* bytes it needs are the ones beside the picture's own at the same position.
|
|
16
|
+
*
|
|
17
|
+
* The answer always carries `subtitles`, `null` included, so the page can tell
|
|
18
|
+
* a proxy that weighed the chosen track from one that never heard of it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} forecast - What `predictPlaybackReadiness` answered.
|
|
23
|
+
* @param {{ fileIndex: number, trackIndex: number | null } | null} selection -
|
|
24
|
+
* The subtitle this viewer chose, for the file being opened; null for none.
|
|
25
|
+
* @param {boolean} ready - Whether that subtitle has been read where they stand.
|
|
26
|
+
* @returns {object} The forecast, holding for the subtitle where it must.
|
|
27
|
+
*/
|
|
28
|
+
export function withSubtitleReadiness(forecast, selection, ready) {
|
|
29
|
+
const subtitles = selection
|
|
30
|
+
? { fileIndex: selection.fileIndex, trackIndex: selection.trackIndex, ready: ready === true }
|
|
31
|
+
: null;
|
|
32
|
+
if (forecast?.ready !== true || !subtitles || subtitles.ready) {
|
|
33
|
+
return { ...forecast, subtitles };
|
|
34
|
+
}
|
|
35
|
+
// No delay: the forecast's figure is the picture's, which is already met, and
|
|
36
|
+
// no rate predicts when a read of a few cues finishes.
|
|
37
|
+
return { ...forecast, ready: false, delaySeconds: null, reason: "subtitles-pending", subtitles };
|
|
38
|
+
}
|
|
@@ -572,6 +572,9 @@ export function wireOutputs({
|
|
|
572
572
|
// When this viewer's playback can start and run to the end: the viewer
|
|
573
573
|
// component's forecast, which models the viewer's browser.
|
|
574
574
|
playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, RateTrend },
|
|
575
|
+
// Whether the subtitle a viewer chose has been read where they stand — the
|
|
576
|
+
// source preparation's answer, set on these parts by the assembly.
|
|
577
|
+
subtitleReadyFor: (viewer) => parts.subtitleReadyFor?.(viewer) ?? true,
|
|
575
578
|
sourceInputsFor: (output, index) => parts.sourceInputsFor?.(output, index),
|
|
576
579
|
encodeSpeedReadingOf: (output) => parts.encodeCost.latestSpeedReadingOf(output),
|
|
577
580
|
projectedEncodeSpeedOf: (output) => parts.encodeCost.projectedSpeedOf(output),
|
|
@@ -104,14 +104,26 @@ export class SourcePreparation {
|
|
|
104
104
|
work.positions === this.#positionsFor(work.sourceKey, work.fileIndex, viewer.id));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Whether the subtitle this viewer chose has been read where they stand.
|
|
109
|
+
*
|
|
110
|
+
* Playback readiness waits on this (`subtitle-readiness.js`), so every way
|
|
111
|
+
* the read can END must answer it, and a refusal is one of them. The cues
|
|
112
|
+
* are the last of four reads — the track table, the media info, the packet
|
|
113
|
+
* index, then the cues — and a refusal at an earlier step means the later
|
|
114
|
+
* ones are never asked. Counting only the cues' own answer left a track the
|
|
115
|
+
* proxy cannot read waiting for ever.
|
|
116
|
+
*/
|
|
107
117
|
subtitleReadyFor(viewer) {
|
|
108
118
|
const selection = viewer.subtitle;
|
|
109
119
|
if (!selection) return true;
|
|
120
|
+
const positions = () => this.#positionsFor(selection.sourceKey, selection.fileIndex, viewer.id, true);
|
|
110
121
|
return [...this.#work.values()].some(work => work.sourceKey === selection.sourceKey &&
|
|
111
122
|
work.fileIndex === selection.fileIndex && ["result", "terminal"].includes(work.result) &&
|
|
112
123
|
(selection.trackIndex === null ? work.statement === "subtitle-file" && work.ownerFileIndex === viewer.source.selectedFileIndex
|
|
113
|
-
: work.
|
|
114
|
-
work.positions ===
|
|
124
|
+
: work.role === "subtitle-embedded" && work.ownerId === viewer.id && work.trackIndex === selection.trackIndex &&
|
|
125
|
+
(work.positions === undefined || work.positions === positions()) &&
|
|
126
|
+
(work.statement === "subtitle-cues" || work.result === "terminal")));
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
async bytesChanged(sourceKey, fileIndex) {
|