@torrent-tv/proxy 2.94.1 → 2.94.3
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
|
@@ -21,16 +21,7 @@ export function h264Configuration(avcc) {
|
|
|
21
21
|
|
|
22
22
|
function readSps(nal, includeOrder = false) {
|
|
23
23
|
if ((nal[0] & 31) !== 7 || nal[0] & 128) throw new Error("AVC configuration entry is not an SPS.");
|
|
24
|
-
const
|
|
25
|
-
for (let at = 1, zeroes = 0; at < nal.length; at++) {
|
|
26
|
-
const byte = nal[at];
|
|
27
|
-
if (zeroes >= 2 && byte === 3) {
|
|
28
|
-
if (at + 1 >= nal.length || nal[at + 1] > 3) throw new Error("AVC emulation prevention byte is invalid.");
|
|
29
|
-
zeroes = 0; continue;
|
|
30
|
-
}
|
|
31
|
-
bytes.push(byte); zeroes = byte === 0 ? zeroes + 1 : 0;
|
|
32
|
-
}
|
|
33
|
-
const bits = new Bits(Buffer.from(bytes));
|
|
24
|
+
const bits = new Bits(unescapeNal(nal));
|
|
34
25
|
const profile = bits.uint(8), constraints = bits.uint(8), level = bits.uint(8);
|
|
35
26
|
bits.ue();
|
|
36
27
|
let chroma = 1, separate = 0, depth = 8;
|
|
@@ -131,14 +122,16 @@ export function avcOrderParameters(sps, pps) {
|
|
|
131
122
|
return { ...facts.pictureOrder, bottomFieldOrderPresent: bits.uint(1) === 1 };
|
|
132
123
|
}
|
|
133
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Remove every emulation prevention byte: a 0x03 after two zero bytes, whatever follows it. H.264 §7.4.1
|
|
127
|
+
* forbids 0x000003 before a byte above 0x03, but encoders write it (LostFilm's LostCoder) and decoders drop
|
|
128
|
+
* it (ffmpeg's ff_h2645_extract_rbsp), so refusing it refuses files every player plays.
|
|
129
|
+
*/
|
|
134
130
|
export function unescapeNal(nal) {
|
|
135
131
|
const bytes = [];
|
|
136
132
|
for (let at = 1, zeroes = 0; at < nal.length; at++) {
|
|
137
133
|
const byte = nal[at];
|
|
138
|
-
if (zeroes >= 2 && byte === 3) {
|
|
139
|
-
if (at + 1 >= nal.length || nal[at + 1] > 3) throw new Error("AVC escape byte is invalid.");
|
|
140
|
-
zeroes = 0; continue;
|
|
141
|
-
}
|
|
134
|
+
if (zeroes >= 2 && byte === 3) { zeroes = 0; continue; }
|
|
142
135
|
bytes.push(byte);
|
|
143
136
|
zeroes = byte === 0 ? zeroes + 1 : 0;
|
|
144
137
|
}
|
|
@@ -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) {
|