@torrent-tv/proxy 2.65.0 → 2.66.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 +5 -0
- package/package.json +1 -1
- package/server.js +24 -1
- package/services/hls-session-manager.js +83 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2.66.0
|
|
2
|
+
|
|
3
|
+
- **New**: A soundtrack that ships beside the picture is fetched WHOLE once the swarm has capacity to spare, so a later switch to it does not wait. It is a twentieth of the picture — 30 MB against 566 MB on the field torrent — and having it on disk is the difference between an instant switch and one that pays for its own first pieces. The moment it starts is not a guess about the swarm: it is when the encoder is already as far ahead of the viewer as `--lookahead` lets it get, which is the cushion figure the session already measures and prints. Once per file, and only for a soundtrack in a file of its own.
|
|
4
|
+
- **Chore**: That fetch is a bounded READ of the file's length, deliberately not `file.select()`. Selecting a whole file alongside the readers' own moving windows is the mistake `#syncSelections` in `torrent-pool.js` was written against — a claim covering everything always outranked the window, and a seek to 89.1 % of a 4.7 GB film waited 93 s while the swarm fetched 2.47 GB in file order. The comment there records it; this goes through the same path the edge warm-up uses.
|
|
5
|
+
|
|
1
6
|
## 2.65.0
|
|
2
7
|
|
|
3
8
|
- **New**: A soundtrack that ships as its own file beside the picture is offered like any other. Releases commonly put a dub in `Rus Sound/<name>.mka` and subtitles in `Sub/[team]/<name>.ass`; until now the video played with its original sound and nothing said the rest of the release existed. The pairing is by name — equal base names, or a shared release hash — and a torrent holding exactly one picture takes every sidecar beside it, since there is nothing else they could belong to. A torrent with several episodes never relaxes that: a wrong pairing would put the sound of one episode over the picture of another, and nothing downstream could notice.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -192,7 +192,30 @@ export async function startProxyServer({
|
|
|
192
192
|
getCachedMediaInfo: (params) => playbackPlanner.getCachedMediaInfo(params),
|
|
193
193
|
// The file's audio tracks, for the master playlist's rendition group. Already
|
|
194
194
|
// probed for the browser's audio menu; read from there rather than probed again.
|
|
195
|
-
getCachedAudioTracks: (params) => playbackPlanner.getCachedAudioTracks(params)
|
|
195
|
+
getCachedAudioTracks: (params) => playbackPlanner.getCachedAudioTracks(params),
|
|
196
|
+
// Pull one whole file onto the disk. Used for a soundtrack that ships beside
|
|
197
|
+
// the picture, once the encoder is as far ahead of the viewer as it is
|
|
198
|
+
// allowed to get — the one moment the swarm's capacity is demonstrably
|
|
199
|
+
// spare. A bounded read of the file's whole length, NOT `file.select()`:
|
|
200
|
+
// selecting a file alongside the readers' own windows is what made a seek
|
|
201
|
+
// wait 93 s while the swarm fetched 2.47 GB in file order (see
|
|
202
|
+
// `#syncSelections` in `torrent-pool.js`).
|
|
203
|
+
fetchWholeFile: async ({ sourceKey, fileIndex }) => {
|
|
204
|
+
const record = sourceRegistry.get(sourceKey);
|
|
205
|
+
if (!record) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const torrent = await torrentPool.getTorrent(record.sourceType, record.source);
|
|
209
|
+
const file = torrent?.files?.[fileIndex];
|
|
210
|
+
if (!file || !Number.isFinite(file.length) || file.length <= 0) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await torrentPool.prefetchFileEdges(torrent, fileIndex, {
|
|
214
|
+
headBytes: file.length,
|
|
215
|
+
tailBytes: 0,
|
|
216
|
+
timeoutMs: 600_000
|
|
217
|
+
});
|
|
218
|
+
}
|
|
196
219
|
});
|
|
197
220
|
const playbackPlanner = createPlaybackPlanner({
|
|
198
221
|
ffmpegBin,
|
|
@@ -1626,6 +1626,7 @@ export class HlsSessionManager {
|
|
|
1626
1626
|
tonemapSupported = false,
|
|
1627
1627
|
getCachedMediaInfo = null,
|
|
1628
1628
|
getCachedAudioTracks = null,
|
|
1629
|
+
fetchWholeFile = null,
|
|
1629
1630
|
segmentFormatId = undefined,
|
|
1630
1631
|
stateDir = "",
|
|
1631
1632
|
getTorrentTotals}) {
|
|
@@ -1645,6 +1646,12 @@ export class HlsSessionManager {
|
|
|
1645
1646
|
// The file's audio tracks, for the master playlist's rendition group. Same
|
|
1646
1647
|
// inventory the browser's audio menu is built from.
|
|
1647
1648
|
this.getCachedAudioTracks = typeof getCachedAudioTracks === "function" ? getCachedAudioTracks : null;
|
|
1649
|
+
// Fetch one whole file of a source, as a bounded read rather than a
|
|
1650
|
+
// selection. Used to pull a soundtrack that ships beside the picture onto
|
|
1651
|
+
// the disk while the swarm has capacity to spare — see
|
|
1652
|
+
// `#fetchSpareSoundtracks`. Optional: a proxy wired without it simply reads
|
|
1653
|
+
// such a soundtrack when it is played.
|
|
1654
|
+
this.fetchWholeFile = typeof fetchWholeFile === "function" ? fetchWholeFile : null;
|
|
1648
1655
|
// Optional async accessor for a source's live download stats, used by the
|
|
1649
1656
|
// realtime budget to tell a CPU limit from a download-starved input:
|
|
1650
1657
|
// (sourceKey, fileIndex) => Promise<{ downloadSpeed, fileLength, fileProgress } | null>.
|
|
@@ -3588,6 +3595,82 @@ export class HlsSessionManager {
|
|
|
3588
3595
|
`${viewers} viewer(s) holding up to ` +
|
|
3589
3596
|
`${deepestBuffer === null ? "?" : deepestBuffer.toFixed(1)}s`
|
|
3590
3597
|
);
|
|
3598
|
+
this.#fetchSpareSoundtracks(session, aheadOfPicture);
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
/**
|
|
3602
|
+
* Fetch the soundtracks that ship beside this picture, whole, while the swarm
|
|
3603
|
+
* has capacity to spare.
|
|
3604
|
+
*
|
|
3605
|
+
* WHY IT WAITS FOR THE CUSHION. A soundtrack nobody has chosen is worth having
|
|
3606
|
+
* on disk — it is a twentieth of the picture (30 MB against 566 MB on the
|
|
3607
|
+
* field torrent) and having it makes every later switch instant instead of
|
|
3608
|
+
* paying for its first pieces. But fetching it takes swarm capacity from the
|
|
3609
|
+
* picture, and there is exactly one moment when that capacity is demonstrably
|
|
3610
|
+
* spare: when the encoder is already as far ahead of the viewer as it is
|
|
3611
|
+
* allowed to get. That is not a guess about the swarm — it is the measurement
|
|
3612
|
+
* the line above just printed.
|
|
3613
|
+
*
|
|
3614
|
+
* WHY IT IS A READ AND NOT A SELECTION. `file.select()` claims every piece of
|
|
3615
|
+
* a file at once, and `#syncSelections` in `torrent-pool.js` records what that
|
|
3616
|
+
* cost when it was done alongside the readers' own windows: a claim covering
|
|
3617
|
+
* everything always outranked the window, and a seek to 89.1% of a 4.7 GB film
|
|
3618
|
+
* waited 93 s while the swarm fetched 2.47 GB in file order. So this goes
|
|
3619
|
+
* through the same bounded read the edge warm-up uses, which claims a moving
|
|
3620
|
+
* window like any other reader and gives it back when it ends.
|
|
3621
|
+
*
|
|
3622
|
+
* Once per file, and only for a soundtrack in a file of its own — the
|
|
3623
|
+
* picture's own tracks are already in the bytes being played.
|
|
3624
|
+
*
|
|
3625
|
+
* @param {HlsSession} session
|
|
3626
|
+
* @param {number} aheadOfPicture - Seconds of film ready ahead of the viewer.
|
|
3627
|
+
* @returns {void}
|
|
3628
|
+
*/
|
|
3629
|
+
#fetchSpareSoundtracks(session, aheadOfPicture) {
|
|
3630
|
+
if (typeof this.fetchWholeFile !== "function") {
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
// The encoder is held at this distance and no further, so reaching it is the
|
|
3634
|
+
// signal that nothing more is being asked of the swarm on the picture's
|
|
3635
|
+
// behalf.
|
|
3636
|
+
if (!(aheadOfPicture >= this.lookaheadSeconds)) {
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const inventory = this.getCachedAudioTracks?.({
|
|
3640
|
+
sourceKey: session.sourceKey,
|
|
3641
|
+
fileIndex: session.fileIndex
|
|
3642
|
+
}) ?? [];
|
|
3643
|
+
if (!(this.spareSoundtracksFetched instanceof Set)) {
|
|
3644
|
+
this.spareSoundtracksFetched = new Set();
|
|
3645
|
+
}
|
|
3646
|
+
const wanted = new Set(
|
|
3647
|
+
inventory
|
|
3648
|
+
.filter((entry) => entry?.kind === "sidecar" && Number.isInteger(entry.fileIndex))
|
|
3649
|
+
.map((entry) => entry.fileIndex)
|
|
3650
|
+
);
|
|
3651
|
+
for (const fileIndex of wanted) {
|
|
3652
|
+
const key = `${session.sourceKey}:${fileIndex}`;
|
|
3653
|
+
if (this.spareSoundtracksFetched.has(key)) {
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3656
|
+
this.spareSoundtracksFetched.add(key);
|
|
3657
|
+
logger.info(
|
|
3658
|
+
`transcode ${session.id.slice(0, 8)} the picture is ${Math.round(aheadOfPicture)}s ahead of ` +
|
|
3659
|
+
`the viewer, so file ${fileIndex} — a soundtrack beside it — is fetched whole now; ` +
|
|
3660
|
+
"a switch to it will not wait for the swarm"
|
|
3661
|
+
);
|
|
3662
|
+
// Not awaited: nothing depends on it finishing, and a failure costs only
|
|
3663
|
+
// that the switch pays for its own pieces, as it did before this existed.
|
|
3664
|
+
Promise.resolve(this.fetchWholeFile({ sourceKey: session.sourceKey, fileIndex })).catch(
|
|
3665
|
+
(error) => {
|
|
3666
|
+
logger.info(
|
|
3667
|
+
`transcode: fetching soundtrack file ${fileIndex} whole failed ` +
|
|
3668
|
+
`(${error instanceof Error ? error.message : String(error)}) — ` +
|
|
3669
|
+
"it will be read when it is played"
|
|
3670
|
+
);
|
|
3671
|
+
}
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3591
3674
|
}
|
|
3592
3675
|
|
|
3593
3676
|
/**
|