@torrent-tv/proxy 2.88.6 → 2.89.1
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 +2285 -2267
- 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 +836 -765
- 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/test/whole-file-held-read.test.js +30 -0
- package/utils/coalesce.js +41 -0
|
@@ -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.
|