@torrent-tv/proxy 2.88.6 → 2.89.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/AGENTS.md +21 -12
- package/CHANGELOG.md +17 -3
- package/assets/calibration/audio/NOTICE.md +19 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/docs/container-architecture.md +77 -22
- package/docs/playback-start-forecast.md +56 -56
- package/main2.cpuprofile +1 -0
- package/package.json +1 -1
- package/scripts/build-audio-samples.mjs +31 -31
- package/server.js +42 -27
- package/services/encode/OutputOpening.js +5 -4
- package/services/encode/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- package/services/media/ContainerOrchestrator.js +99 -46
- package/services/media/KeyframeTables.js +61 -21
- package/services/media/SubtitleCues.js +255 -183
- package/services/media/container/AviContainer.js +5 -3
- package/services/media/container/Container.js +66 -43
- package/services/media/container/ContainerFactory.js +8 -26
- package/services/media/container/MatroskaContainer.js +719 -1182
- package/services/media/container/Mp4Container.js +83 -73
- package/services/media/container/ebml-stream.js +174 -0
- package/services/media/container/matroska-clusters.js +1165 -0
- package/services/media/container/unavailable.js +77 -0
- package/services/media/tracks/ContainerTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +9 -1
- package/services/server/playback-readiness.js +516 -516
- package/services/torrent/worker/client.js +6 -2
- package/services/torrent/worker/held-bytes.js +84 -8
- package/services/torrent/worker/worker.js +10 -2
- package/services/transport/data-channel-handler.js +7 -3
- package/services/viewer/PriorityMap.js +31 -0
- package/test/audio-calibration.test.js +23 -23
- package/test/coalesce.test.js +39 -0
- package/test/held-read-ownership.test.js +77 -0
- package/test/helpers/held-file.js +50 -2
- package/test/helpers/matroska-file.js +296 -0
- package/test/keyframe-table-budget.test.js +21 -0
- package/test/matroska-cluster-discovery.test.js +323 -0
- package/test/reads-not-yet-arrived.test.js +218 -0
- package/test/subtitle-cue-walk.test.js +16 -1
- package/test/subtitle-plan-waits.test.js +125 -0
- package/test/subtitle-track-numbering.test.js +8 -3
- package/test/viewer-starts-on-map.test.js +27 -0
- package/utils/coalesce.js +41 -0
package/AGENTS.md
CHANGED
|
@@ -93,17 +93,25 @@ are recorded in `docs/browser-proxy-contract.md`.
|
|
|
93
93
|
`ExternalSubtitleFile`. Spec-accurate flags (FlagForced only on subtitles per
|
|
94
94
|
RFC 9559 §5.1.4.1, FlagOriginal/Commentary only on audio, tkhd
|
|
95
95
|
track_enabled / alternate_group, elng BCP47).
|
|
96
|
-
- `media/` application files: `ContainerOrchestrator` (detect +
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
- `media/` application files: `ContainerOrchestrator` (detect + the ONE
|
|
97
|
+
container per file, `containerFor`/`getTracks`/`getMediaInfo`/`getKeyframeIndex`;
|
|
98
|
+
the container keeps its own reading of each element it states — the Segment
|
|
99
|
+
layout, Tracks, Cues, MP4's `moov` — once it has been read), `KeyframeTables`,
|
|
100
100
|
`SubtitleOrchestrator` (the track list and the cues behind
|
|
101
101
|
`Container` tracks, warm/push — it is handed the walk, and `server.js` is
|
|
102
|
-
what puts the two together), `SubtitleCues` (the
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
what puts the two together), `SubtitleCues` (the plan, the found-order
|
|
103
|
+
cursor, one walk of a file at a time, taking back withdrawn cues). It uses
|
|
104
|
+
the file's one container — it no longer builds its own. The walk itself is
|
|
105
|
+
`MatroskaContainer.readHeldCues` over `container/matroska-clusters.js` (which
|
|
106
|
+
also finds clusters the Cues table does not name) and
|
|
107
|
+
`Mp4Container.readHeldSamples`; what the torrent supplies is `held-bytes.js`
|
|
108
|
+
— which ranges are downloaded whole, and a read of one that never fetches
|
|
109
|
+
and answers at once with nothing where the range is not whole.
|
|
110
|
+
- **A read whose bytes have not arrived is not an answer about the file.**
|
|
111
|
+
Every container read is strict (`container/unavailable.js`): every byte, or
|
|
112
|
+
`BytesUnavailable`. A value, a proven absence, or a refusal of ours with its
|
|
113
|
+
reason may be kept; "not here yet" never is — not by a container, not by
|
|
114
|
+
`ContainerOrchestrator`, not by `KeyframeTables`, not by `SubtitleCues`.
|
|
107
115
|
**`KeyframeTables` is where a file's keyframe table lives and the only thing
|
|
108
116
|
that reads one.** The table is `container/KeyframeTable.js`, ONE object per
|
|
109
117
|
file, handed out rather than copied — so a read that lands after a session
|
|
@@ -113,9 +121,10 @@ are recorded in `docs/browser-proxy-contract.md`.
|
|
|
113
121
|
file that must be re-encoded for ever from a passing shortage of bytes off
|
|
114
122
|
the swarm. One read per file whoever asks, the second asker joins the first,
|
|
115
123
|
each caller's wait is bounded by the measured `KEYFRAME_TABLE_BUDGET_MS`
|
|
116
|
-
while the READ is not, and a read that threw
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
while the READ is not, and a read that threw — `BytesUnavailable` included —
|
|
125
|
+
is never recorded as an answer; it is read again when pieces of the file
|
|
126
|
+
arrive (`readAgainIfUnanswered`). It is the ONLY store of the keyframe
|
|
127
|
+
answer; the container keeps the Cues reading the times are taken from. The second reader is the packet probe in
|
|
119
128
|
`media/keyframe-probe.js`, which decodes rather than parsing; the fuller of the
|
|
120
129
|
two answers is the one that stands (`media/keyframe-probe.js`).
|
|
121
130
|
- `server/controllers/` — interface layer: `PlaybackController` / `SubtitleController`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
## 2.89.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: A read whose bytes have not arrived is no longer kept as a statement about the file. Every container read is strict — every byte asked for, or `BytesUnavailable` — and only a value or a proven absence is kept. This applies to the container choice, the track table, Matroska's Cues table, MP4's `moov`, AVI's `idx1`, the media info, the keyframe table and the subtitle plan. An embedded subtitle track whose Cues table was still downloading used to be planned with no clusters for the life of the process and show nothing for a whole session. The keyframe table had the same defect and lost the copy branch for such a file; it is now read again when pieces of the file arrive.
|
|
4
|
+
- **Fix**: One container per file. The subtitle walk no longer builds its own container and reads the head and the Cues table a second time; the Tracks element is parsed once; the Cues table is read once and gives both the keyframe times and the subtitle clusters; MP4's `moov` is read once for every question.
|
|
5
|
+
- **New**: Subtitle clusters are found from the downloaded bytes when the Cues table does not name them (RFC 9559 §22.1 only recommends it) or when there is no Cues table. Positions come from the file (first cluster, every CueClusterPosition) and from a chain of top-level elements. Where the file has no Cues, a search of the bytes finds candidates, and a candidate is used only after its whole structure checks out. A candidate that a cluster established later turns out to contain is taken back together with its cues, and the browser is told which cues to remove.
|
|
6
|
+
- **New**: Matroska elements are read where they lie and by their own size, one portion (the torrent's piece length) at a time. A large Cues table, a Tracks element larger than the head window and a large cluster are no longer cut short by a fixed window; data that is not needed is skipped without being assembled.
|
|
7
|
+
- **New**: The cluster a viewer stands in is read first, then the one before it, and pushed at once; the rest is read in later passes, and a pass stops when a viewer moves.
|
|
8
|
+
- **Fix**: A Matroska track with no `Language` element is English, as RFC 9559 §5.1.4.1.19 and RFC 8794 §11.1.19 require. `LanguageBCP47` still wins, an explicit `und` stays unknown, and where the language came from travels as `languageSource`.
|
|
9
|
+
- **Fix**: A read of what is downloaded answers at once with nothing when the range is not wholly downloaded. It used to open the torrent's own file stream, which selects pieces outside the demand register and waited up to 30 s. At a torrent's open, 36 such reads competed with the file being watched. The bytes it returns are handed to the main thread with their memory transferred instead of copied.
|
|
10
|
+
- **Fix**: A subtitle walk asked for while one is running runs once more afterwards instead of being dropped.
|
|
11
|
+
- **Fix**: The walk reads a cluster's structure — headers and the first bytes of each block — and a subtitle block's data by its own size, so a picture's frames never cross into the main thread, and each read hands the loop back before the next. Measured on the addon host over a 287 MB file: memory growth 10-15 MB instead of 75-95 MB, and the main loop's 99th-percentile delay 13-17 ms against 12-13 ms with no walk (20-32 ms before).
|
|
12
|
+
- **Fix**: The search for clusters in a file with no Cues decides each candidate as it finds it and follows an accepted one by its chain, so a file whose clusters follow one another is no longer searched whole and then read a second time to check each cluster. The first pass on that file took 0.3-0.7 s instead of 4-6 s.
|
|
13
|
+
- **Fix**: A keyframe table whose bytes have not arrived makes an opening re-encode instead of failing it; the table is read again when pieces of the file arrive.
|
|
14
|
+
|
|
1
15
|
## 2.88.6
|
|
2
16
|
|
|
3
|
-
- **Fix**: Validate closed segment coverage against the same cut-time delta passed to FFmpeg. Complete audio segments that close before the requested grid boundary remain usable; truncated segments still fail validation.
|
|
4
|
-
|
|
17
|
+
- **Fix**: Validate closed segment coverage against the same cut-time delta passed to FFmpeg. Complete audio segments that close before the requested grid boundary remain usable; truncated segments still fail validation.
|
|
18
|
+
|
|
5
19
|
## 2.88.5
|
|
6
20
|
|
|
7
21
|
- **Fix**: Reject closed non-final fragments that do not reach their next declared cut, using measured coded-frame coverage. Remove such previously cached fragments from the inventory so missing media can be produced again instead of indefinitely blocking startup.
|
|
@@ -2267,4 +2281,4 @@ All eight were found by reading the file after the torrent worker was killed by
|
|
|
2267
2281
|
|
|
2268
2282
|
- **New**: WebRTC P2P tunnel architecture — replaced direct HTTP streaming with a persistent WebSocket tunnel to the server. Video is delivered from the proxy to the browser over a WebRTC data channel; the server acts only as a signalling relay.
|
|
2269
2283
|
- **New**: `node-datachannel` dependency for server-side WebRTC.
|
|
2270
|
-
- **Removed**: `public_base_url` config — no longer needed.
|
|
2284
|
+
- **Removed**: `public_base_url` config — no longer needed.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
# Audio calibration samples
|
|
2
|
-
|
|
3
|
-
These files contain deterministic synthetic pink noise, encoded at build time
|
|
4
|
-
by `scripts/build-audio-samples.mjs`. They contain no recorded third-party audio.
|
|
5
|
-
The manifest records the actual duration, channels, sample rate and byte count
|
|
6
|
-
reported by ffprobe. They are distributed under the package's GPL-3.0-or-later
|
|
7
|
-
license.
|
|
8
|
-
|
|
9
|
-
Startup reads each packaged sample once for packet copying and once for AAC
|
|
10
|
-
processing. It neither generates input files nor loops them. The operation
|
|
11
|
-
timeout is the existing encoder benchmark timeout; it is not an ETA coefficient.
|
|
12
|
-
|
|
13
|
-
Process startup is measured separately from FFmpeg's reported processing time;
|
|
14
|
-
its printed clock precision bounds runtime uncertainty. Input reading is
|
|
15
|
-
included in processing. For encoding, the
|
|
16
|
-
reference work scales by input audio samples (channels multiplied by sample
|
|
17
|
-
rate); for copying, a declared source bitrate scales encoded-byte work. These
|
|
18
|
-
are approximate initial rates, replaced by actual per-output progress. Unknown
|
|
19
|
-
dimensions retain the measured reference rather than fabricated metadata.
|
|
1
|
+
# Audio calibration samples
|
|
2
|
+
|
|
3
|
+
These files contain deterministic synthetic pink noise, encoded at build time
|
|
4
|
+
by `scripts/build-audio-samples.mjs`. They contain no recorded third-party audio.
|
|
5
|
+
The manifest records the actual duration, channels, sample rate and byte count
|
|
6
|
+
reported by ffprobe. They are distributed under the package's GPL-3.0-or-later
|
|
7
|
+
license.
|
|
8
|
+
|
|
9
|
+
Startup reads each packaged sample once for packet copying and once for AAC
|
|
10
|
+
processing. It neither generates input files nor loops them. The operation
|
|
11
|
+
timeout is the existing encoder benchmark timeout; it is not an ETA coefficient.
|
|
12
|
+
|
|
13
|
+
Process startup is measured separately from FFmpeg's reported processing time;
|
|
14
|
+
its printed clock precision bounds runtime uncertainty. Input reading is
|
|
15
|
+
included in processing. For encoding, the
|
|
16
|
+
reference work scales by input audio samples (channels multiplied by sample
|
|
17
|
+
rate); for copying, a declared source bitrate scales encoded-byte work. These
|
|
18
|
+
are approximate initial rates, replaced by actual per-output progress. Unknown
|
|
19
|
+
dimensions retain the measured reference rather than fabricated metadata.
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"codec": "aac",
|
|
4
|
-
"file": "aac.m4a",
|
|
5
|
-
"durationSeconds": 4,
|
|
6
|
-
"channels": 2,
|
|
7
|
-
"samplingFrequency": 48000,
|
|
8
|
-
"bytes": 66060
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"codec": "ac3",
|
|
12
|
-
"file": "ac3.ac3",
|
|
13
|
-
"durationSeconds": 4,
|
|
14
|
-
"channels": 2,
|
|
15
|
-
"samplingFrequency": 48000,
|
|
16
|
-
"bytes": 96000
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"codec": "eac3",
|
|
20
|
-
"file": "eac3.eac3",
|
|
21
|
-
"durationSeconds": 4,
|
|
22
|
-
"channels": 2,
|
|
23
|
-
"samplingFrequency": 48000,
|
|
24
|
-
"bytes": 96000
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"codec": "dts",
|
|
28
|
-
"file": "dts.dts",
|
|
29
|
-
"durationSeconds": 4.0051,
|
|
30
|
-
"channels": 2,
|
|
31
|
-
"samplingFrequency": 48000,
|
|
32
|
-
"bytes": 706500
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"codec": "flac",
|
|
36
|
-
"file": "flac.flac",
|
|
37
|
-
"durationSeconds": 4,
|
|
38
|
-
"channels": 2,
|
|
39
|
-
"samplingFrequency": 48000,
|
|
40
|
-
"bytes": 723158
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"codec": "opus",
|
|
44
|
-
"file": "opus.opus",
|
|
45
|
-
"durationSeconds": 4.0065,
|
|
46
|
-
"channels": 2,
|
|
47
|
-
"samplingFrequency": 48000,
|
|
48
|
-
"bytes": 32457
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"codec": "vorbis",
|
|
52
|
-
"file": "vorbis.ogg",
|
|
53
|
-
"durationSeconds": 4,
|
|
54
|
-
"channels": 2,
|
|
55
|
-
"samplingFrequency": 48000,
|
|
56
|
-
"bytes": 42108
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"codec": "truehd",
|
|
60
|
-
"file": "truehd.thd",
|
|
61
|
-
"durationSeconds": null,
|
|
62
|
-
"channels": 2,
|
|
63
|
-
"samplingFrequency": 48000,
|
|
64
|
-
"bytes": 589492
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
"codec": "mp3",
|
|
68
|
-
"file": "mp3.mp3",
|
|
69
|
-
"durationSeconds": 4,
|
|
70
|
-
"channels": 2,
|
|
71
|
-
"samplingFrequency": 48000,
|
|
72
|
-
"bytes": 64748
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"codec": "mp2",
|
|
76
|
-
"file": "mp2.mp2",
|
|
77
|
-
"durationSeconds": 4.008,
|
|
78
|
-
"channels": 2,
|
|
79
|
-
"samplingFrequency": 48000,
|
|
80
|
-
"bytes": 192384
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"codec": "alac",
|
|
84
|
-
"file": "alac.m4a",
|
|
85
|
-
"durationSeconds": 4,
|
|
86
|
-
"channels": 2,
|
|
87
|
-
"samplingFrequency": 48000,
|
|
88
|
-
"bytes": 712871
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
"codec": "pcm",
|
|
92
|
-
"file": "pcm.wav",
|
|
93
|
-
"durationSeconds": 4,
|
|
94
|
-
"channels": 2,
|
|
95
|
-
"samplingFrequency": 48000,
|
|
96
|
-
"bytes": 768078
|
|
97
|
-
}
|
|
98
|
-
]
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"codec": "aac",
|
|
4
|
+
"file": "aac.m4a",
|
|
5
|
+
"durationSeconds": 4,
|
|
6
|
+
"channels": 2,
|
|
7
|
+
"samplingFrequency": 48000,
|
|
8
|
+
"bytes": 66060
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"codec": "ac3",
|
|
12
|
+
"file": "ac3.ac3",
|
|
13
|
+
"durationSeconds": 4,
|
|
14
|
+
"channels": 2,
|
|
15
|
+
"samplingFrequency": 48000,
|
|
16
|
+
"bytes": 96000
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"codec": "eac3",
|
|
20
|
+
"file": "eac3.eac3",
|
|
21
|
+
"durationSeconds": 4,
|
|
22
|
+
"channels": 2,
|
|
23
|
+
"samplingFrequency": 48000,
|
|
24
|
+
"bytes": 96000
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"codec": "dts",
|
|
28
|
+
"file": "dts.dts",
|
|
29
|
+
"durationSeconds": 4.0051,
|
|
30
|
+
"channels": 2,
|
|
31
|
+
"samplingFrequency": 48000,
|
|
32
|
+
"bytes": 706500
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"codec": "flac",
|
|
36
|
+
"file": "flac.flac",
|
|
37
|
+
"durationSeconds": 4,
|
|
38
|
+
"channels": 2,
|
|
39
|
+
"samplingFrequency": 48000,
|
|
40
|
+
"bytes": 723158
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"codec": "opus",
|
|
44
|
+
"file": "opus.opus",
|
|
45
|
+
"durationSeconds": 4.0065,
|
|
46
|
+
"channels": 2,
|
|
47
|
+
"samplingFrequency": 48000,
|
|
48
|
+
"bytes": 32457
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"codec": "vorbis",
|
|
52
|
+
"file": "vorbis.ogg",
|
|
53
|
+
"durationSeconds": 4,
|
|
54
|
+
"channels": 2,
|
|
55
|
+
"samplingFrequency": 48000,
|
|
56
|
+
"bytes": 42108
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"codec": "truehd",
|
|
60
|
+
"file": "truehd.thd",
|
|
61
|
+
"durationSeconds": null,
|
|
62
|
+
"channels": 2,
|
|
63
|
+
"samplingFrequency": 48000,
|
|
64
|
+
"bytes": 589492
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"codec": "mp3",
|
|
68
|
+
"file": "mp3.mp3",
|
|
69
|
+
"durationSeconds": 4,
|
|
70
|
+
"channels": 2,
|
|
71
|
+
"samplingFrequency": 48000,
|
|
72
|
+
"bytes": 64748
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"codec": "mp2",
|
|
76
|
+
"file": "mp2.mp2",
|
|
77
|
+
"durationSeconds": 4.008,
|
|
78
|
+
"channels": 2,
|
|
79
|
+
"samplingFrequency": 48000,
|
|
80
|
+
"bytes": 192384
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"codec": "alac",
|
|
84
|
+
"file": "alac.m4a",
|
|
85
|
+
"durationSeconds": 4,
|
|
86
|
+
"channels": 2,
|
|
87
|
+
"samplingFrequency": 48000,
|
|
88
|
+
"bytes": 712871
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"codec": "pcm",
|
|
92
|
+
"file": "pcm.wav",
|
|
93
|
+
"durationSeconds": 4,
|
|
94
|
+
"channels": 2,
|
|
95
|
+
"samplingFrequency": 48000,
|
|
96
|
+
"bytes": 768078
|
|
97
|
+
}
|
|
98
|
+
]
|
|
@@ -58,6 +58,35 @@ So ffmpeg keeps exactly three jobs, and nothing else:
|
|
|
58
58
|
That is a final answer about the container, and the point at which a caller may
|
|
59
59
|
go to the media — not "unknown, ask again".
|
|
60
60
|
|
|
61
|
+
## A read that has not arrived is not an answer
|
|
62
|
+
|
|
63
|
+
Every read a container makes goes through `strictReader` (`container/unavailable.js`):
|
|
64
|
+
it answers with every byte asked for, or throws `BytesUnavailable`. A reading has
|
|
65
|
+
therefore four outcomes — the value; a proven absence ("this file has no Cues");
|
|
66
|
+
a refusal of ours, stated with its reason and size (an MP4 `moov` larger than
|
|
67
|
+
this program reads whole: `moovRefused`); and "not here yet". Only the first
|
|
68
|
+
three are facts about the file, and only they are kept — by the container, by
|
|
69
|
+
`ContainerOrchestrator`, by `KeyframeTables` and by `SubtitleCues`.
|
|
70
|
+
|
|
71
|
+
Until 2026-10-01 "not here yet" came back as `null`, the value the readers
|
|
72
|
+
return for an absent element. A subtitle plan read while its Cues table was
|
|
73
|
+
downloading was kept as "no clusters", and an embedded track showed nothing for
|
|
74
|
+
a whole session; the keyframe table, the container choice, the track table,
|
|
75
|
+
MP4's `moov` and AVI's `idx1` had the same shape
|
|
76
|
+
(`research/subtitles-never-appear-2026-10-01.md`).
|
|
77
|
+
|
|
78
|
+
## Elements are read where they lie
|
|
79
|
+
|
|
80
|
+
Matroska's top-level elements are found by walking their HEADERS from the
|
|
81
|
+
Segment's start to the first cluster, each costing only its header, and from the
|
|
82
|
+
SeekHead for what lies after the clusters — so a Cues element stored before the
|
|
83
|
+
clusters with no SeekHead entry (RFC 9559 §6.4) is found. Elements are then read
|
|
84
|
+
by their own size through `container/ebml-stream.js`, one portion at a time; the
|
|
85
|
+
portion is the torrent's piece length, handed in, never chosen here. Data that
|
|
86
|
+
is not needed is skipped without being assembled; a needed element larger than
|
|
87
|
+
one portion is refused by name. There is no fixed window for the Cues table,
|
|
88
|
+
the Tracks element or a cluster any more.
|
|
89
|
+
|
|
61
90
|
## Where byte access lives, and why not on a track
|
|
62
91
|
|
|
63
92
|
A `ContainerTrack` is a DECLARATION. It carries no `readRange` and no file
|
|
@@ -77,26 +106,52 @@ bytes — was reviewed on 2026-09-03 and is **not** the right shape:
|
|
|
77
106
|
on the thread where the container is already at hand.
|
|
78
107
|
|
|
79
108
|
**That split is closed.** `SubtitleTrack` carries `clusterPositions` and
|
|
80
|
-
`samples` — byte POSITIONS — and the reading of those positions is
|
|
81
|
-
container's: `MatroskaContainer.
|
|
82
|
-
`Mp4Container.readHeldSamples
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
The
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
`samples` — byte POSITIONS — and the reading of those positions is the
|
|
110
|
+
container's: `MatroskaContainer.readHeldCues` (over `container/matroska-clusters.js`)
|
|
111
|
+
and `Mp4Container.readHeldSamples`, beside `readTracks`, `readKeyframeIndex`,
|
|
112
|
+
`readMediaInfo` and `cueTextOf`.
|
|
113
|
+
|
|
114
|
+
The two readers want different read POLICIES over the same file: the track
|
|
115
|
+
table fetches what is missing from the swarm, while the cue walk reads only
|
|
116
|
+
what is already downloaded, so that turning subtitles on never pulls bytes the
|
|
117
|
+
viewer is not waiting for. **One container per file serves both**: it is built
|
|
118
|
+
with the fetching `readRange`, and the walk's reader of downloaded bytes — which
|
|
119
|
+
ranges are whole, a strict read of them, the portion, where viewers stand — is
|
|
120
|
+
handed to `readHeldCues` on every pass (`HeldReader`), because what is
|
|
121
|
+
downloaded changes between passes. The walk used to build a second container of
|
|
122
|
+
its own over the same file and read the head and the Cues table again, under a
|
|
123
|
+
different rule for a read that had not arrived.
|
|
124
|
+
|
|
125
|
+
What the container is NOT given is the torrent. `media/SubtitleCues.js` is
|
|
126
|
+
handed a `HeldFile` of plain functions by `server.js` and keeps only what is
|
|
127
|
+
genuinely its own: the found-order cursor a browser follows, the per-file
|
|
128
|
+
state, one walk of a file at a time, and taking back the cues of a cluster the
|
|
129
|
+
container withdraws.
|
|
130
|
+
|
|
131
|
+
## Where a cluster is, when the Cues table does not say
|
|
132
|
+
|
|
133
|
+
RFC 9559 §22.1 only recommends that each subtitle frame be referenced by the
|
|
134
|
+
Cues table, and a file may have none. `container/matroska-clusters.js` finds
|
|
135
|
+
clusters from the downloaded bytes: positions the file establishes (the first
|
|
136
|
+
cluster, every `CueClusterPosition` of every track), a chain of top-level
|
|
137
|
+
elements from each established one, and — only where the file has no Cues — a
|
|
138
|
+
search for the Cluster id. What the search finds is a candidate; it is used only
|
|
139
|
+
once every child is one §5.1.3 allows, exactly one Timestamp is present, the
|
|
140
|
+
children fill it exactly, every block names a declared track, and any CRC-32 or
|
|
141
|
+
PrevSize matches. A candidate that an established cluster turns out to contain
|
|
142
|
+
is withdrawn with its cues, and the browser removes them by number. The cluster
|
|
143
|
+
a viewer stands in is read first, then the one before it, and pushed at once.
|
|
144
|
+
|
|
145
|
+
The walk reads a cluster's STRUCTURE — every child's header and the first bytes
|
|
146
|
+
of each block, which name its track — and a subtitle block's data by its own
|
|
147
|
+
size. It never moves a picture's frames into the main thread: measured on the
|
|
148
|
+
addon host 2026-10-01, reading clusters a portion at a time moved every frame
|
|
149
|
+
of the film across the thread boundary, and the collector then took three times
|
|
150
|
+
as long as the walk. Each read also hands the loop back before the walk goes on,
|
|
151
|
+
because the torrent thread replies faster than the message port empties. The
|
|
152
|
+
search decides each candidate as it finds it; one accepted is followed by its
|
|
153
|
+
chain, and the search resumes where the chain ends, so a file whose clusters
|
|
154
|
+
follow one another is searched only up to the first cluster after each gap.
|
|
100
155
|
|
|
101
156
|
## Layers
|
|
102
157
|
|
|
@@ -166,10 +221,10 @@ flowchart TB
|
|
|
166
221
|
- `ContainerFactory.create({readRange,fileSize})` — sniffs 16 bytes, returns precise `Container` subclass. No torrent knowledge.
|
|
167
222
|
- `ContainerOrchestrator` — per-file cache (`sourceKey:fileIndex`), `getTracks()` / `getMediaInfo()` / `getKeyframeIndex()`. Transport-agnostic.
|
|
168
223
|
- **The keyframe table is read once per file, and the container is not what remembers it.** `Container.readKeyframeIndex` parses and returns; each format implements `parseKeyframeIndex`. It used to keep the answer, and that made this the one fact stored in two places — here and in the file's `KeyframeTable` on the main thread. That copy had no reader, and since the parse itself moved to the main thread there is no second thread for a second copy to sit in.
|
|
169
|
-
- **`KeyframeTable` / `KeyframeTables` — the answer, and the policy around getting one.** The table is one object per file (`media/container/KeyframeTable.js`), held by everyone who reads that file rather than copied, so a read that lands after a session was made still reaches it. `media/KeyframeTables.js` reads it once per file whoever asks, joins the second asker to the read already running, bounds how long any one caller waits (`KEYFRAME_TABLE_BUDGET_MS`, measured), and never records a read that threw as an answer. Its reader is one function handed in at construction, so it knows nothing of torrents or HTTP. It is not `ContainerOrchestrator
|
|
224
|
+
- **`KeyframeTable` / `KeyframeTables` — the answer, and the policy around getting one.** The table is one object per file (`media/container/KeyframeTable.js`), held by everyone who reads that file rather than copied, so a read that lands after a session was made still reaches it. `media/KeyframeTables.js` reads it once per file whoever asks, joins the second asker to the read already running, bounds how long any one caller waits (`KEYFRAME_TABLE_BUDGET_MS`, measured), and never records a read that threw as an answer. Its reader is one function handed in at construction, so it knows nothing of torrents or HTTP. It is not `ContainerOrchestrator`: both live on the main thread, but that one keeps the file's container — which keeps its own Cues reading — and this keeps the answer the sessions hold and the wait around it. A read that has not arrived rejects with `BytesUnavailable` and is read again when pieces of the file arrive (`readAgainIfUnanswered`).
|
|
170
225
|
- **`answered` and `readable` are different questions.** `answered` says a reader came back; `readable` says it came back with times. A file that answered nothing must be re-encoded for ever (MPEG-TS: 669 real keyframes, no index of any kind); a file that has not answered is a shortage of bytes off the swarm. Held as loose fields in a bag of probe results nothing told them apart, and a passing shortage was written onto the file as a property of the bytes.
|
|
171
226
|
- **The second reader is the packet probe** (`media/keyframe-probe.js`): it finds keyframes by decoding, for containers that state no index. It is never waited for, and `KeyframeTable.learn` keeps the fuller answer whichever arrives second — measured 2026-08-02, a scan found 77 keyframes in 45 s without finishing against all 570 in 0.8 s from the index.
|
|
172
|
-
- `SubtitleOrchestrator` — wraps `
|
|
227
|
+
- `SubtitleOrchestrator` — wraps `media/SubtitleCues.js` (`cuesHeldFor`, `warmSubtitleCues`, `subtitleTracksOf`) behind the `ContainerTrack` abstraction, handed in by `server.js`. Routes depend on this. The reading itself is the file's one container's; `SubtitleCues` keeps the cursor and the per-file state.
|
|
173
228
|
- `PlaybackController` / `SubtitleController` — thin interface adapters; `routes/api/*` delegate to them, handle HTTP headers (`X-Subtitle-Language`, `X-Subtitle-Cursor`) only.
|
|
174
229
|
|
|
175
230
|
## Legacy
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
# Playback start forecast
|
|
2
|
-
|
|
3
|
-
For resource r, measured service is S_r(t) = rate_r * t. New observations
|
|
4
|
-
replace the conditional future rate; polling cannot invent acceleration,
|
|
5
|
-
exponential recovery or a new physical measurement.
|
|
6
|
-
|
|
7
|
-
Source work uses the file's measured byte density, missing local byte ranges
|
|
8
|
-
and the observed torrent download rate. Shared input ranges are downloaded
|
|
9
|
-
once. Files on the same torrent share a queue; distinct torrents use distinct
|
|
10
|
-
observed services. Local input reading is included in effective processing
|
|
11
|
-
speed, rather than charged again as a separate disk or memory delay.
|
|
12
|
-
|
|
13
|
-
Each output's remaining processing work subtracts progress only within its
|
|
14
|
-
actual live run intervals. Initial video rates use existing codec/size/encoder
|
|
15
|
-
calibration. Initial audio rates use short packaged per-codec measurements and
|
|
16
|
-
sample work (encoding) or encoded bytes (copying). Actual output progress
|
|
17
|
-
replaces these initial rates. Startup is charged once, separately from processing
|
|
18
|
-
throughput, and disappears once actual run progress establishes processing.
|
|
19
|
-
|
|
20
|
-
Input arrival and processing overlap. With arrival time D(m), processing rate
|
|
21
|
-
e and previous completion C, completion for an interval [a,b] is
|
|
22
|
-
|
|
23
|
-
max(C + (b-a)/e, sup_m(D(m) + (b-m)/e)).
|
|
24
|
-
|
|
25
|
-
Between service boundaries these functions are linear, so only endpoints are
|
|
26
|
-
needed. Client delivery follows production and shares the observed link queue.
|
|
27
|
-
Measured client ranges and prepared media establish each track's deadlines.
|
|
28
|
-
|
|
29
|
-
The minimum start delay for this conditional trajectory is
|
|
30
|
-
|
|
31
|
-
max(0, max_j(completion_j - presentation_deadline_j)).
|
|
32
|
-
|
|
33
|
-
This covers the remaining finite movie, including rates below real time. Slow
|
|
34
|
-
positive service gives a finite preproduction delay; it is not rejected by a
|
|
35
|
-
browser-capacity threshold or a capped search. Observed supply-interruption
|
|
36
|
-
coverage is held in prepared proxy media and client media, without requiring
|
|
37
|
-
the browser to hold it all.
|
|
38
|
-
|
|
39
|
-
Critical review: future rates can change, average byte density is an initial
|
|
40
|
-
mapping rather than an exact packet index, initial audio sample-work scaling
|
|
41
|
-
does not model every decoder's nonlinear cost, and client receive/append time
|
|
42
|
-
is not separately measured. These are approximation limits, not hidden weights.
|
|
43
|
-
Actual progress, segment sizes and link observations refine the prediction.
|
|
44
|
-
The client countdown uses elapsed local monotonic time; expiration cannot
|
|
45
|
-
authorize playback. Only the proxy's readiness and real track coverage can.
|
|
46
|
-
|
|
47
|
-
Zero service, missing media facts and a genuine timestamp gap remain explicit
|
|
48
|
-
noncomputable states. The UI reports their causes instead of an estimating
|
|
49
|
-
placeholder or a fabricated duration. Small coded-frame joins follow measured
|
|
50
|
-
frame durations and Chromium's continuous-track rule, not a hand-picked margin.
|
|
51
|
-
|
|
52
|
-
A closed non-final fragment is reusable only if its measured coded-frame
|
|
53
|
-
coverage reaches its next declared cut. Interrupted one-frame fragments are
|
|
54
|
-
excluded on publication and when reading an existing cache, so the production
|
|
55
|
-
schedule treats their media as unfinished work. The final fragment uses its
|
|
56
|
-
actual media end rather than the container's approximate duration.
|
|
1
|
+
# Playback start forecast
|
|
2
|
+
|
|
3
|
+
For resource r, measured service is S_r(t) = rate_r * t. New observations
|
|
4
|
+
replace the conditional future rate; polling cannot invent acceleration,
|
|
5
|
+
exponential recovery or a new physical measurement.
|
|
6
|
+
|
|
7
|
+
Source work uses the file's measured byte density, missing local byte ranges
|
|
8
|
+
and the observed torrent download rate. Shared input ranges are downloaded
|
|
9
|
+
once. Files on the same torrent share a queue; distinct torrents use distinct
|
|
10
|
+
observed services. Local input reading is included in effective processing
|
|
11
|
+
speed, rather than charged again as a separate disk or memory delay.
|
|
12
|
+
|
|
13
|
+
Each output's remaining processing work subtracts progress only within its
|
|
14
|
+
actual live run intervals. Initial video rates use existing codec/size/encoder
|
|
15
|
+
calibration. Initial audio rates use short packaged per-codec measurements and
|
|
16
|
+
sample work (encoding) or encoded bytes (copying). Actual output progress
|
|
17
|
+
replaces these initial rates. Startup is charged once, separately from processing
|
|
18
|
+
throughput, and disappears once actual run progress establishes processing.
|
|
19
|
+
|
|
20
|
+
Input arrival and processing overlap. With arrival time D(m), processing rate
|
|
21
|
+
e and previous completion C, completion for an interval [a,b] is
|
|
22
|
+
|
|
23
|
+
max(C + (b-a)/e, sup_m(D(m) + (b-m)/e)).
|
|
24
|
+
|
|
25
|
+
Between service boundaries these functions are linear, so only endpoints are
|
|
26
|
+
needed. Client delivery follows production and shares the observed link queue.
|
|
27
|
+
Measured client ranges and prepared media establish each track's deadlines.
|
|
28
|
+
|
|
29
|
+
The minimum start delay for this conditional trajectory is
|
|
30
|
+
|
|
31
|
+
max(0, max_j(completion_j - presentation_deadline_j)).
|
|
32
|
+
|
|
33
|
+
This covers the remaining finite movie, including rates below real time. Slow
|
|
34
|
+
positive service gives a finite preproduction delay; it is not rejected by a
|
|
35
|
+
browser-capacity threshold or a capped search. Observed supply-interruption
|
|
36
|
+
coverage is held in prepared proxy media and client media, without requiring
|
|
37
|
+
the browser to hold it all.
|
|
38
|
+
|
|
39
|
+
Critical review: future rates can change, average byte density is an initial
|
|
40
|
+
mapping rather than an exact packet index, initial audio sample-work scaling
|
|
41
|
+
does not model every decoder's nonlinear cost, and client receive/append time
|
|
42
|
+
is not separately measured. These are approximation limits, not hidden weights.
|
|
43
|
+
Actual progress, segment sizes and link observations refine the prediction.
|
|
44
|
+
The client countdown uses elapsed local monotonic time; expiration cannot
|
|
45
|
+
authorize playback. Only the proxy's readiness and real track coverage can.
|
|
46
|
+
|
|
47
|
+
Zero service, missing media facts and a genuine timestamp gap remain explicit
|
|
48
|
+
noncomputable states. The UI reports their causes instead of an estimating
|
|
49
|
+
placeholder or a fabricated duration. Small coded-frame joins follow measured
|
|
50
|
+
frame durations and Chromium's continuous-track rule, not a hand-picked margin.
|
|
51
|
+
|
|
52
|
+
A closed non-final fragment is reusable only if its measured coded-frame
|
|
53
|
+
coverage reaches its next declared cut. Interrupted one-frame fragments are
|
|
54
|
+
excluded on publication and when reading an existing cache, so the production
|
|
55
|
+
schedule treats their media as unfinished work. The final fragment uses its
|
|
56
|
+
actual media end rather than the container's approximate duration.
|