@torrent-tv/proxy 2.95.1 → 2.95.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/assets/calibration/NOTICE.md +15 -0
- package/assets/calibration/cal-encode-reference.mp4 +0 -0
- package/package.json +1 -1
- package/server.js +6 -2
- package/services/encode/hwaccel.js +16 -1
- package/services/media/container/MatroskaContainer.js +5 -3
- package/services/media/container/Mp4Container.js +10 -2
- package/services/media/playback-declarations.js +6 -2
- package/services/media/tracks/ContainerTrack.js +23 -0
|
@@ -42,6 +42,21 @@ The MPEG-4 Part 2 set is decoded by the same decoder (`mpeg4`) as an XviD or
|
|
|
42
42
|
DivX 4+ release. VC-1 has no set because ffmpeg has no VC-1 encoder, and
|
|
43
43
|
10-bit AV1 has none yet; both are priced as H.264, and the startup log says so.
|
|
44
44
|
|
|
45
|
+
## The encode reference clip
|
|
46
|
+
|
|
47
|
+
`cal-encode-reference.mp4` is 3 s of **"Chimera"**, also Netflix Open Content
|
|
48
|
+
under CC BY 4.0, from `AV1/Chimera/Old/Chimera-AV1-8bit-1920x1080-6736kbps.mp4`
|
|
49
|
+
at 250 s, re-encoded with `libx264 -preset slow -crf 21 -profile:v high -g 72`,
|
|
50
|
+
1920×1080, 24 fps, no audio. Every startup encode reading is taken on it.
|
|
51
|
+
|
|
52
|
+
It is the middle of that film by how hard it is to encode, and that film was
|
|
53
|
+
the hardest of four measured on the addon host on 2026-10-07 (Meridian, Chimera,
|
|
54
|
+
Sparks, Cosmos Laundromat): its typical stretch cost 1.30x the processor time per
|
|
55
|
+
second of video of the Meridian clip used before at `ultrafast` and 2.2x at
|
|
56
|
+
`fast`. Encoded at `-crf 12` instead, the same stretch cost the same within 2 %.
|
|
57
|
+
(torrent-tv/meta#3, `research/startup-calibration-spread-2026-10-07.md` in
|
|
58
|
+
`torrent-tv/meta`.)
|
|
59
|
+
|
|
45
60
|
## Why six, and why this grid
|
|
46
61
|
|
|
47
62
|
`services/encode/hwaccel.js` decodes them at startup and fits the host's decode cost,
|
|
Binary file
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -60,6 +60,7 @@ import { DownloadMaps } from "./services/viewer/DownloadMaps.js";
|
|
|
60
60
|
import { nativeSourceMap } from "./services/viewer/NativeSourceMap.js";
|
|
61
61
|
import { MediaReadRequests } from "./services/media/MediaReadRequests.js";
|
|
62
62
|
import { SegmentInputs } from "./services/media/SegmentInputs.js";
|
|
63
|
+
import { ContainerTrack } from "./services/media/tracks/ContainerTrack.js";
|
|
63
64
|
import { pauseCoefficient, viewerStartsOn } from "./services/viewer/PriorityMap.js";
|
|
64
65
|
import { coalescing } from "./utils/coalesce.js";
|
|
65
66
|
import {
|
|
@@ -581,7 +582,7 @@ export async function startProxyServer({
|
|
|
581
582
|
const spec = output.spec[type];
|
|
582
583
|
if (!spec) continue;
|
|
583
584
|
const list = selected.get(spec.fileIndex) ?? [];
|
|
584
|
-
list.push({ type, index: type === "video" ?
|
|
585
|
+
list.push({ type, index: type === "video" ? null : spec.trackIndex,
|
|
585
586
|
mode: type === "video" ? spec.encode ? "transcode" : "copy" : spec.transcode ? "transcode" : "copy" });
|
|
586
587
|
selected.set(spec.fileIndex, list);
|
|
587
588
|
}
|
|
@@ -595,7 +596,10 @@ export async function startProxyServer({
|
|
|
595
596
|
if (typeof container?.readPacketIndex !== "function") return { kind: "needs-index", reason: "packet-index-not-yet-available" };
|
|
596
597
|
const modes = new Map(), wanted = [];
|
|
597
598
|
for (const choice of choices) {
|
|
598
|
-
|
|
599
|
+
// The picture is the first usable video track (torrent-tv/meta#49);
|
|
600
|
+
// a soundtrack keeps the number every list of them uses.
|
|
601
|
+
const track = choice.type === "video" ? ContainerTrack.firstUsable(tracks.value, "video")
|
|
602
|
+
: tracks.value.filter(track => track.type === choice.type)[choice.index];
|
|
599
603
|
if (!track) return { kind: "terminal", reason: "selected-track-is-absent" };
|
|
600
604
|
wanted.push(track); modes.set(track, choice.mode);
|
|
601
605
|
}
|
|
@@ -506,7 +506,22 @@ const CALIBRATION_SETS = {
|
|
|
506
506
|
"cal-av1-480-lo.mp4"
|
|
507
507
|
]
|
|
508
508
|
};
|
|
509
|
-
|
|
509
|
+
/**
|
|
510
|
+
* The footage every encode reading is taken on: 3 s of Netflix Open Content
|
|
511
|
+
* "Chimera" (CC BY 4.0), from the middle of that film by how hard it is to
|
|
512
|
+
* encode (torrent-tv/meta#3).
|
|
513
|
+
*
|
|
514
|
+
* It used to be the first decode clip, 2 s of "Meridian". Measured on the addon
|
|
515
|
+
* host on 2026-10-07, against the hardest and the middle stretches of four films,
|
|
516
|
+
* that clip was the cheapest of them all to encode: a typical stretch of Chimera
|
|
517
|
+
* took 1.32x its processor time per second of video at `ultrafast` and 2.42x at
|
|
518
|
+
* `fast`, so every startup figure read a machine faster than it is on an
|
|
519
|
+
* ordinary film. The hardest film's typical stretch is the baseline, not its
|
|
520
|
+
* hardest five seconds: the offer is about the speed a machine holds for a whole
|
|
521
|
+
* film, and a short hard stretch is what the viewer's cushion is for
|
|
522
|
+
* (`research/startup-calibration-spread-2026-10-07.md`).
|
|
523
|
+
*/
|
|
524
|
+
export const CALIBRATION_REFERENCE_CLIP = "cal-encode-reference.mp4";
|
|
510
525
|
export const CALIBRATION_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "calibration");
|
|
511
526
|
// How wide the measured window must be before the slope is trusted, and how
|
|
512
527
|
// long to wait for it at most.
|
|
@@ -604,7 +604,7 @@ export class MatroskaContainer extends Container {
|
|
|
604
604
|
// picture cues alone cannot prove that an earlier subtitle has ended.
|
|
605
605
|
if (requested.every(track => track.type === "subtitle")) return first;
|
|
606
606
|
const cueTrack = requested.find(track => track.type === "video") ??
|
|
607
|
-
|
|
607
|
+
ContainerTrack.firstUsable(tracks, "video") ?? requested[0];
|
|
608
608
|
if (!cueTrack) return first;
|
|
609
609
|
const points = (cues?.points ?? []).flatMap(point => point.positions
|
|
610
610
|
.filter(position => position.track === cueTrack.trackNumber && position.clusterAt >= first && position.clusterAt < layout.segmentEnd)
|
|
@@ -823,7 +823,7 @@ export class MatroskaContainer extends Container {
|
|
|
823
823
|
}
|
|
824
824
|
|
|
825
825
|
/**
|
|
826
|
-
* The keyframe times of the first video track, from the one Cues reading.
|
|
826
|
+
* The keyframe times of the first usable video track, from the one Cues reading.
|
|
827
827
|
*
|
|
828
828
|
* A CuePoint belongs to the track named inside its CueTrackPositions, and a
|
|
829
829
|
* muxer indexes whatever tracks it likes — field files index their subtitle
|
|
@@ -845,7 +845,9 @@ export class MatroskaContainer extends Container {
|
|
|
845
845
|
return null;
|
|
846
846
|
}
|
|
847
847
|
const tracks = await this.readTracks();
|
|
848
|
-
|
|
848
|
+
// The picture's own entries: a video track the file marks unusable is not
|
|
849
|
+
// played, so its keyframes are not where the picture can be cut.
|
|
850
|
+
const video = ContainerTrack.firstUsable(tracks, "video")?.trackNumber ?? null;
|
|
849
851
|
const timesFor = (wanted) =>
|
|
850
852
|
cues.points
|
|
851
853
|
.filter((point) => wanted === null || point.positions.some((position) => position.track === wanted))
|
|
@@ -21,6 +21,7 @@ import { VideoTrack } from "../tracks/VideoTrack.js";
|
|
|
21
21
|
import { AudioTrack } from "../tracks/AudioTrack.js";
|
|
22
22
|
import { TextSubtitleTrack, TEXT_FORMATS_MP4 } from "../tracks/TextSubtitleTrack.js";
|
|
23
23
|
import { ImageSubtitleTrack } from "../tracks/ImageSubtitleTrack.js";
|
|
24
|
+
import { ContainerTrack } from "../tracks/ContainerTrack.js";
|
|
24
25
|
import { isUnavailable } from "./unavailable.js";
|
|
25
26
|
import { PacketIndex } from "./PacketIndex.js";
|
|
26
27
|
import { h264Configuration } from "./h264-configuration.js";
|
|
@@ -565,7 +566,7 @@ export class Mp4Container extends Container {
|
|
|
565
566
|
this.fragmentState ??= {};
|
|
566
567
|
index = await readMp4Fragments({ readRange: this.readRange, fileSize: this.fileSize,
|
|
567
568
|
tracks: fragmentTracks(held, tracks), memory: this.packetMemory, state: this.fragmentState });
|
|
568
|
-
const timeline =
|
|
569
|
+
const timeline = ContainerTrack.firstUsable(tracks, "video") ?? ContainerTrack.firstUsable(tracks, "audio");
|
|
569
570
|
const bounds = timeline && index.boundsOf(timeline.trackNumber);
|
|
570
571
|
if (bounds) this.mediaInfo = { ...(await this.readMediaInfo()), startTimeSeconds: bounds.start,
|
|
571
572
|
durationSeconds: bounds.end - bounds.start };
|
|
@@ -842,7 +843,7 @@ export class Mp4Container extends Container {
|
|
|
842
843
|
async parseKeyframeIndex() {
|
|
843
844
|
const held = await this.#moovBuffer();
|
|
844
845
|
if (held && childOf(held.moov, held.header, held.moov.length, "mvex")) {
|
|
845
|
-
const video = (await this.readTracks()
|
|
846
|
+
const video = ContainerTrack.firstUsable(await this.readTracks(), "video");
|
|
846
847
|
return video ? { times: (await this.readPacketIndex()).keyframesOf(video.trackNumber), tolerance: 0 } : null;
|
|
847
848
|
}
|
|
848
849
|
const r = held ? keyframeTimesFromMoov(held.moov, held.header) : null;
|
|
@@ -1664,6 +1665,13 @@ function keyframeTimesFromMoov(moov, headerBytes) {
|
|
|
1664
1665
|
if (!mdia || !isVideoTrack(moov, mdia)) {
|
|
1665
1666
|
continue;
|
|
1666
1667
|
}
|
|
1668
|
+
// "A disabled track (the low bit is zero) is treated as if it were not
|
|
1669
|
+
// present" (ISO/IEC 14496-12 §8.3.2), so it is not the picture whose
|
|
1670
|
+
// keyframes decide the cuts (torrent-tv/meta#49).
|
|
1671
|
+
const tkhd = findBox(moov, trak.dataOffset, trak.end, "tkhd");
|
|
1672
|
+
if (tkhd && tkhd.dataOffset + 4 <= tkhd.end && (moov.readUIntBE(tkhd.dataOffset + 1, 3) & 0x000001) === 0) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1667
1675
|
const mdhd = findBox(moov, mdia.dataOffset, mdia.end, "mdhd");
|
|
1668
1676
|
if (!mdhd) {
|
|
1669
1677
|
continue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AudioTrack } from "./tracks/AudioTrack.js";
|
|
2
2
|
import { TextSubtitleTrack } from "./tracks/TextSubtitleTrack.js";
|
|
3
|
+
import { ContainerTrack } from "./tracks/ContainerTrack.js";
|
|
3
4
|
|
|
4
5
|
const VIDEO_CODECS = new Map([
|
|
5
6
|
["V_MPEG4/ISO/AVC", "h264"], ["avc1", "h264"], ["avc3", "h264"],
|
|
@@ -33,7 +34,9 @@ function videoCodecOf(track) {
|
|
|
33
34
|
export function playbackDeclarations({ tracks, media, fileBytes = null }) {
|
|
34
35
|
const ordered = type => tracks.filter(track => track.type === type)
|
|
35
36
|
.sort((a, b) => a.declaredIndex - b.declaredIndex);
|
|
36
|
-
|
|
37
|
+
// The picture is the first video track the container marks usable; a
|
|
38
|
+
// disabled one is not played (torrent-tv/meta#49).
|
|
39
|
+
const video = ContainerTrack.firstUsable(tracks, "video") ?? undefined;
|
|
37
40
|
const inventory = type => ordered(type).map((track, index) => ({
|
|
38
41
|
...track, index, streamIndex: track.declaredIndex,
|
|
39
42
|
codec: type === "audio" ? AudioTrack.codecNameOf(track)
|
|
@@ -46,7 +49,8 @@ export function playbackDeclarations({ tracks, media, fileBytes = null }) {
|
|
|
46
49
|
}));
|
|
47
50
|
const audioTracks = inventory("audio"), subtitleTracks = inventory("subtitle");
|
|
48
51
|
return { tracks, video, audioTracks, subtitleTracks,
|
|
49
|
-
|
|
52
|
+
// The soundtrack a file opens with is its first usable one.
|
|
53
|
+
audioCodec: ContainerTrack.firstUsable(audioTracks, "audio")?.codec ?? "",
|
|
50
54
|
videoCodec: video ? videoCodecOf(video) : "",
|
|
51
55
|
container: media.format,
|
|
52
56
|
durationSeconds: media.durationSeconds,
|
|
@@ -72,6 +72,29 @@ export class ContainerTrack {
|
|
|
72
72
|
this.alternateGroup = Number.isFinite(alternateGroup) ? alternateGroup : 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The first track of `type` the container marks usable, or null.
|
|
77
|
+
*
|
|
78
|
+
* Matroska `FlagEnabled`: "Set to 1 if the track is usable" (RFC 9559
|
|
79
|
+
* §5.1.4.1.3). MP4 `track_enabled`: "A disabled track … is treated as if it
|
|
80
|
+
* were not present" (ISO/IEC 14496-12 §8.3.2). So the picture a file is
|
|
81
|
+
* played with, and the soundtrack it opens with when nobody chose one, are the
|
|
82
|
+
* first USABLE ones — not the first declared. A disabled track keeps its
|
|
83
|
+
* position in the list, because ffmpeg numbers it either way.
|
|
84
|
+
*
|
|
85
|
+
* Takes plain objects as well as tracks: tracks cross the worker boundary as
|
|
86
|
+
* plain objects.
|
|
87
|
+
*
|
|
88
|
+
* @param {Array<{ type: string, isEnabled?: boolean, declaredIndex?: number }>} tracks
|
|
89
|
+
* @param {"video" | "audio" | "subtitle"} type
|
|
90
|
+
* @returns {object | null}
|
|
91
|
+
*/
|
|
92
|
+
static firstUsable(tracks, type) {
|
|
93
|
+
return (Array.isArray(tracks) ? tracks : [])
|
|
94
|
+
.filter((track) => track?.type === type && track.isEnabled !== false)
|
|
95
|
+
.sort((a, b) => (a.declaredIndex ?? 0) - (b.declaredIndex ?? 0))[0] ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
/** Whether this track should be offered in UI menus. Base rule: disabled tracks hidden but still counted for declaredIndex. */
|
|
76
99
|
isOfferable() {
|
|
77
100
|
return this.isEnabled;
|