@torrent-tv/proxy 2.44.0 → 2.45.0
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/services/container-index/matroska-subtitles.js +24 -4
- package/services/hls-session-manager.js +35 -3
- package/services/playback-planner.js +574 -538
- package/services/subtitle-defaults.js +131 -0
- package/services/torrent-worker/pool-adapter.js +18 -0
- package/services/torrent-worker/subtitle-cues.js +25 -1
- package/services/torrent-worker/worker.js +5 -2
- package/test/cuts-follow-published-grid.test.js +73 -0
- package/test/subtitle-defaults.test.js +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.45.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: A run is cut where the PLAYER was told the cuts are. There are two boundary tables — the live one, corrected as produced segments reveal where the file's cuts truly are, and the one the playlist text was written from — and a player places every fragment by the text it holds, which never changes. The cut list handed to ffmpeg came from the live table, so every correction moved the run away from the timeline the player is reading: measured 2026-08-20 on `Minions.and.Monsters.1080p.mkv`, 119 of 125 produced segments arrived a uniform 2.002 s before the times their playlist named, against the 0.5 s hls.js bridges, and a fragment that does not land is fetched again — on 2026-08-17 two of them 1908 times each. A seek is resolved on the same table for the same reason: the time being resolved came out of that playlist. The corrections keep their purpose, which is to describe the FILE — a variant created later inherits the corrected table and publishes it, so its own playlist and its own cuts agree from the start — but they may no longer move the cuts of a session already being read.
|
|
4
|
+
- **New**: `FlagDefault` is read from the container itself rather than from ffmpeg's description of it. Matroska's flag DEFAULTS TO 1 and ffmpeg has applied that default by the time it prints `(default)`, so a file whose muxer marked no subtitle track is indistinguishable in the banner from one that marked every track — and the difference is the whole question, since one means "show this" and the other means "the file has no opinion". The EBML reader already walks the Tracks element for subtitle extraction and now also records whether the element was WRITTEN. Lining the two readings up is the awkward part and it is checked rather than assumed: ffmpeg numbers `0:s:N` over every subtitle stream in container order, so position is the correspondence, but each pair must agree on language or on title — one that agrees on neither, or a differing count, and the container reading is not used at all, leaving the probe's own flags with nothing claimed for them. `mergeContainerSubtitleFlags` is pure and tested.
|
|
5
|
+
- **Chore**: The line reporting a segment that began away from its grid said "the playlist says", while the figure it prints comes from the live table. Reading that log on 2026-08-20 cost a wrong diagnosis; it now says "the grid says".
|
|
6
|
+
|
|
1
7
|
## 2.44.0
|
|
2
8
|
|
|
3
9
|
- **Fix**: The cues a browser is missing are found by the order they were READ, not by where they sit in the film. Cues come out of whichever clusters are downloaded, and a torrent does not arrive in film order, so the set of known cues grows in the MIDDLE as well as at the end — and the cursor shipped in 2.43.1 was a time. Measured 2026-08-20: a viewer at 272 s was answered with cues out to 1176 s, and from that moment every cue between the two was filtered away for the rest of the session, with 59 of 276 clusters read. The subtitles the viewer was about to need had become unreachable, while cues fifteen minutes ahead kept arriving. Each cue now carries the order it was found in, `?since=<n>` selects by that, and the answer states the next cursor in `X-Subtitle-Cursor`. `?after=<seconds>` still works, for a browser that has not been reloaded.
|
package/package.json
CHANGED
|
@@ -92,7 +92,7 @@ function readString(buffer, element) {
|
|
|
92
92
|
*
|
|
93
93
|
* @param {(start: number, end: number) => Promise<Buffer | null>} readRange
|
|
94
94
|
* @param {number} fileSize
|
|
95
|
-
* @returns {Promise<{ tracks: SubtitleTrackPlan[], secondsPerTick: number, segmentDataOffset: number } | null>}
|
|
95
|
+
* @returns {Promise<{ tracks: SubtitleTrackPlan[], declared: object[], secondsPerTick: number, segmentDataOffset: number } | null>}
|
|
96
96
|
*/
|
|
97
97
|
export async function readSubtitlePlan(readRange, fileSize) {
|
|
98
98
|
const head = await readRange(0, Math.min(HEAD_BYTES, Math.max(0, fileSize - 1)));
|
|
@@ -124,6 +124,15 @@ export async function readSubtitlePlan(readRange, fileSize) {
|
|
|
124
124
|
const tracksEnd = Math.min(head.length, tracksElement.dataOffset + tracksElement.size);
|
|
125
125
|
/** @type {SubtitleTrackPlan[]} */
|
|
126
126
|
const tracks = [];
|
|
127
|
+
/**
|
|
128
|
+
* Every subtitle track the file declares, in the order the Tracks element
|
|
129
|
+
* names them, text or picture. This is not for extraction — `tracks` is —
|
|
130
|
+
* but for lining ffmpeg's `0:s:N` numbering up against the container, which
|
|
131
|
+
* only holds while nothing is missing from the middle of the list.
|
|
132
|
+
*
|
|
133
|
+
* @type {Array<{ trackNumber: number, codecId: string, language: string, name: string, isDefault: boolean, declaresDefault: boolean }>}
|
|
134
|
+
*/
|
|
135
|
+
const declared = [];
|
|
127
136
|
for (const entry of iterateElements(head, tracksElement.dataOffset, tracksEnd)) {
|
|
128
137
|
if (entry.id !== ID_TRACK_ENTRY) {
|
|
129
138
|
continue;
|
|
@@ -135,7 +144,13 @@ export async function readSubtitlePlan(readRange, fileSize) {
|
|
|
135
144
|
let language = "";
|
|
136
145
|
let name = "";
|
|
137
146
|
let codecPrivate = "";
|
|
147
|
+
// Matroska's `FlagDefault` DEFAULTS TO 1, so a file whose muxer wrote it on
|
|
148
|
+
// no track is indistinguishable, once the default has been applied, from
|
|
149
|
+
// one that wrote it on every track — which is how ffmpeg's banner prints it
|
|
150
|
+
// and why the banner cannot answer this. Both are kept: what the flag
|
|
151
|
+
// amounts to, and whether the file said anything at all.
|
|
138
152
|
let isDefault = true;
|
|
153
|
+
let declaresDefault = false;
|
|
139
154
|
for (const field of iterateElements(head, entry.dataOffset, entryEnd)) {
|
|
140
155
|
if (field.id === ID_TRACK_NUMBER) {
|
|
141
156
|
trackNumber = readUint(head, field.dataOffset, field.size);
|
|
@@ -149,11 +164,16 @@ export async function readSubtitlePlan(readRange, fileSize) {
|
|
|
149
164
|
name = readString(head, field);
|
|
150
165
|
} else if (field.id === ID_FLAG_DEFAULT) {
|
|
151
166
|
isDefault = readUint(head, field.dataOffset, field.size) === 1;
|
|
167
|
+
declaresDefault = true;
|
|
152
168
|
} else if (field.id === ID_CODEC_PRIVATE) {
|
|
153
169
|
codecPrivate = head.toString("base64", field.dataOffset, field.dataOffset + field.size);
|
|
154
170
|
}
|
|
155
171
|
}
|
|
156
|
-
if (type !== TRACK_TYPE_SUBTITLE || trackNumber === null
|
|
172
|
+
if (type !== TRACK_TYPE_SUBTITLE || trackNumber === null) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
declared.push({ trackNumber, codecId, language, name, isDefault, declaresDefault });
|
|
176
|
+
if (!TEXT_CODECS.has(codecId)) {
|
|
157
177
|
continue;
|
|
158
178
|
}
|
|
159
179
|
tracks.push({
|
|
@@ -167,7 +187,7 @@ export async function readSubtitlePlan(readRange, fileSize) {
|
|
|
167
187
|
});
|
|
168
188
|
}
|
|
169
189
|
if (tracks.length === 0) {
|
|
170
|
-
return { tracks, secondsPerTick: scale / 1e9, segmentDataOffset: base };
|
|
190
|
+
return { tracks, declared, secondsPerTick: scale / 1e9, segmentDataOffset: base };
|
|
171
191
|
}
|
|
172
192
|
|
|
173
193
|
// Where the clusters holding those tracks are. A file that indexes only its
|
|
@@ -232,7 +252,7 @@ export async function readSubtitlePlan(readRange, fileSize) {
|
|
|
232
252
|
}
|
|
233
253
|
}
|
|
234
254
|
}
|
|
235
|
-
return { tracks, secondsPerTick: scale / 1e9, segmentDataOffset: base };
|
|
255
|
+
return { tracks, declared, secondsPerTick: scale / 1e9, segmentDataOffset: base };
|
|
236
256
|
}
|
|
237
257
|
|
|
238
258
|
/**
|
|
@@ -2434,6 +2434,21 @@ export class HlsSessionManager {
|
|
|
2434
2434
|
* @param {number} index
|
|
2435
2435
|
* @returns {number}
|
|
2436
2436
|
*/
|
|
2437
|
+
/**
|
|
2438
|
+
* The boundary table the player is working from: the one its playlist was
|
|
2439
|
+
* written from, falling back to the live table when no playlist was built
|
|
2440
|
+
* from a table at all (no duration, so no synthetic playlist — and then
|
|
2441
|
+
* nothing the player holds contradicts it).
|
|
2442
|
+
*
|
|
2443
|
+
* @param {HlsSession} session
|
|
2444
|
+
* @returns {number[]}
|
|
2445
|
+
*/
|
|
2446
|
+
publishedGridFor(session) {
|
|
2447
|
+
return Array.isArray(session.publishedBoundaries) && session.publishedBoundaries.length > 0
|
|
2448
|
+
? session.publishedBoundaries
|
|
2449
|
+
: (session.segmentBoundaries ?? []);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2437
2452
|
#publishedStartTime(session, index) {
|
|
2438
2453
|
const boundaries = Array.isArray(session.publishedBoundaries) && session.publishedBoundaries.length > 0
|
|
2439
2454
|
? session.publishedBoundaries
|
|
@@ -2486,7 +2501,10 @@ export class HlsSessionManager {
|
|
|
2486
2501
|
* @returns {number}
|
|
2487
2502
|
*/
|
|
2488
2503
|
#segmentIndexForTime(session, t) {
|
|
2489
|
-
|
|
2504
|
+
// The player's grid, for the same reason the cut list uses it: the time
|
|
2505
|
+
// being resolved came from the playlist the player holds, so the index it
|
|
2506
|
+
// means is the index that playlist gives it.
|
|
2507
|
+
const boundaries = this.publishedGridFor(session);
|
|
2490
2508
|
if (boundaries.length < 2) {
|
|
2491
2509
|
return Math.max(0, Math.floor(t / this.segmentDurationSec));
|
|
2492
2510
|
}
|
|
@@ -3797,8 +3815,22 @@ export class HlsSessionManager {
|
|
|
3797
3815
|
// writes no self-contained pieces, so nothing could read a true start and
|
|
3798
3816
|
// segments were stamped with times the file does not have — the 4.17 s
|
|
3799
3817
|
// speech-against-subtitles drift, back again.
|
|
3818
|
+
//
|
|
3819
|
+
// Cut on the grid the PLAYER WAS GIVEN, not on the corrected one. A player
|
|
3820
|
+
// places a fragment by the playlist it holds, and that text was written
|
|
3821
|
+
// once and never changes; the live table keeps moving as produced segments
|
|
3822
|
+
// reveal where the file's cuts really are. Cutting on the moved table makes
|
|
3823
|
+
// every run faithful to a timeline nobody sent the player — measured
|
|
3824
|
+
// 2026-08-20, the picture's segments arriving a uniform 2.002 s before the
|
|
3825
|
+
// times the playlist named for them, which is four times what hls.js will
|
|
3826
|
+
// bridge, so the fragment does not land and is asked for again.
|
|
3827
|
+
//
|
|
3828
|
+
// The corrections keep their purpose: they describe the file, and a variant
|
|
3829
|
+
// created later inherits the corrected table and PUBLISHES it, so its own
|
|
3830
|
+
// playlist and its own cuts agree from the start. What they may not do is
|
|
3831
|
+
// move the cuts of a session whose playlist is already being read.
|
|
3800
3832
|
const gridCutTimes = explicitTimes && (!session.transcodeVideo || session.cutGrid === "keyframe")
|
|
3801
|
-
? segmentCutTimesFrom(session
|
|
3833
|
+
? segmentCutTimesFrom(this.publishedGridFor(session), safeIndex)
|
|
3802
3834
|
: null;
|
|
3803
3835
|
// Cut times are stated on the grid, for both branches.
|
|
3804
3836
|
//
|
|
@@ -5117,7 +5149,7 @@ export class HlsSessionManager {
|
|
|
5117
5149
|
logger.warn(
|
|
5118
5150
|
`transcode ${session.id} segment #${index} really starts at ` +
|
|
5119
5151
|
`${trueStart.toFixed(3)}s (boundary ${at === null ? "none" : `#${at}`}), ` +
|
|
5120
|
-
`the
|
|
5152
|
+
`the grid says ${declaredStart.toFixed(3)}s — ` +
|
|
5121
5153
|
(session.audioOnly === true
|
|
5122
5154
|
// A soundtrack is cut exactly where it was asked to be, so a
|
|
5123
5155
|
// disagreement here is not a reading about the file at all: it is the
|