@torrent-tv/proxy 2.80.1 → 2.80.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/CHANGELOG.md +10 -1
- package/package.json +1 -1
- package/services/encode/run-command.js +697 -662
- package/services/hls-session-manager.js +10626 -10674
- package/services/output/Timeline.js +256 -220
- package/services/output/cut-grid.js +97 -0
- package/test/quality-variants.test.js +1209 -1116
|
@@ -1,220 +1,256 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Where a file is cut, and what the player was told about it.
|
|
3
|
-
*
|
|
4
|
-
* This belongs to the FILE and its grid, not to a session and not to a viewer.
|
|
5
|
-
* Every quality step of one film has to be cut at exactly the same times — that
|
|
6
|
-
* is what lets a segment made by one encoder be appended where another's would
|
|
7
|
-
* have gone — and every session serving that film has to publish the same
|
|
8
|
-
* playlist, or two of them stamp the same moment differently and the picture
|
|
9
|
-
* and the sound drift apart.
|
|
10
|
-
*
|
|
11
|
-
* Until now each session computed and kept its own copy, and agreement between
|
|
12
|
-
* them was achieved by COPYING: a variant was handed `inheritedGrid` at
|
|
13
|
-
* creation, and a soundtrack the same. That works while somebody remembers to
|
|
14
|
-
* pass it, and it failed when the two tables drifted — measured 2026-08-17,
|
|
15
|
-
* corrections of 0.6-2.9 s between two sessions of one file, and again
|
|
16
|
-
* 2026-08-20, segments arriving a uniform 2.002 s before the times the playlist
|
|
17
|
-
* named for them, four times what a player will bridge. One table, held once,
|
|
18
|
-
* cannot drift from itself.
|
|
19
|
-
*
|
|
20
|
-
* **What it does NOT hold, and why.** The container's own keyframe table, how
|
|
21
|
-
* exact that table is, which container answered, and how long the file runs:
|
|
22
|
-
* every one of those is a fact of the FILE, and this object is held per file AND
|
|
23
|
-
* grid — so a file cut two ways kept two copies of one immutable list. They live
|
|
24
|
-
* on the source file now, which is one object per file whatever grids are cut
|
|
25
|
-
* from it. What is left here is the two things that really are per grid: where
|
|
26
|
-
* the cuts are, and what the player was told they are.
|
|
27
|
-
*
|
|
28
|
-
* **Two tables, and they are not the same thing.** `boundaries` is where the
|
|
29
|
-
* file is cut NOW, corrections included, and it is what a run is told to cut
|
|
30
|
-
* at. `published` is what the player was given, written once and never changed,
|
|
31
|
-
* and it is what a segment must be stamped to. A player places a fragment by
|
|
32
|
-
* the playlist it holds; the live table keeps moving as produced segments
|
|
33
|
-
* reveal where the file's cuts really are.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A fresh tally of how well a container's keyframe index matches its file.
|
|
38
|
-
*
|
|
39
|
-
* @returns {{ checked: number, disagreed: number, maxDeviationSec: number, firstDisagreementIndex: number, deviations: number[], landedOnAnotherKeyframe: number, seen: Set<number> }}
|
|
40
|
-
*/
|
|
41
|
-
export function newIndexCheck() {
|
|
42
|
-
return {
|
|
43
|
-
checked: 0,
|
|
44
|
-
disagreed: 0,
|
|
45
|
-
maxDeviationSec: 0,
|
|
46
|
-
firstDisagreementIndex: -1,
|
|
47
|
-
// Every deviation, so the summary can report a distribution instead of one
|
|
48
|
-
// extreme. Bounded by the number of distinct boundaries a file produces.
|
|
49
|
-
deviations: [],
|
|
50
|
-
// Of the segments that started away from the playlist, how many began at
|
|
51
|
-
// ANOTHER time in the very list the grid was built from. This is the
|
|
52
|
-
// measurement that separates the two explanations: a table that describes
|
|
53
|
-
// times the file does not have, against a table that lists only SOME
|
|
54
|
-
// keyframes and a grid built over its gaps. Asked 2026-08-17 by the user,
|
|
55
|
-
// who was right that the second is far more likely — every deviation
|
|
56
|
-
// measured that day was positive, 0.58-2.96 s, which is what a cut pushed
|
|
57
|
-
// forward to the next real keyframe looks like.
|
|
58
|
-
landedOnAnotherKeyframe: 0,
|
|
59
|
-
// Which boundaries have been counted. A segment can be requested again, and
|
|
60
|
-
// a repeat is the same boundary, not new evidence.
|
|
61
|
-
seen: new Set()
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class Timeline {
|
|
66
|
-
/**
|
|
67
|
-
* @param {object} params
|
|
68
|
-
* @param {number[]} params.boundaries - Cut times in seconds, ascending, one
|
|
69
|
-
* more than there are segments.
|
|
70
|
-
* @param {number[] | null} [params.published] - What the player was told, when
|
|
71
|
-
* that differs from the boundaries because corrections have been made since.
|
|
72
|
-
* @param {"keyframe" | "uniform"} params.cutGrid - Whether those times are
|
|
73
|
-
* the source's own keyframes, which a copied picture has no choice about,
|
|
74
|
-
* or an even grid the encoder is told to place keyframes on.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
*
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a file is cut, and what the player was told about it.
|
|
3
|
+
*
|
|
4
|
+
* This belongs to the FILE and its grid, not to a session and not to a viewer.
|
|
5
|
+
* Every quality step of one film has to be cut at exactly the same times — that
|
|
6
|
+
* is what lets a segment made by one encoder be appended where another's would
|
|
7
|
+
* have gone — and every session serving that film has to publish the same
|
|
8
|
+
* playlist, or two of them stamp the same moment differently and the picture
|
|
9
|
+
* and the sound drift apart.
|
|
10
|
+
*
|
|
11
|
+
* Until now each session computed and kept its own copy, and agreement between
|
|
12
|
+
* them was achieved by COPYING: a variant was handed `inheritedGrid` at
|
|
13
|
+
* creation, and a soundtrack the same. That works while somebody remembers to
|
|
14
|
+
* pass it, and it failed when the two tables drifted — measured 2026-08-17,
|
|
15
|
+
* corrections of 0.6-2.9 s between two sessions of one file, and again
|
|
16
|
+
* 2026-08-20, segments arriving a uniform 2.002 s before the times the playlist
|
|
17
|
+
* named for them, four times what a player will bridge. One table, held once,
|
|
18
|
+
* cannot drift from itself.
|
|
19
|
+
*
|
|
20
|
+
* **What it does NOT hold, and why.** The container's own keyframe table, how
|
|
21
|
+
* exact that table is, which container answered, and how long the file runs:
|
|
22
|
+
* every one of those is a fact of the FILE, and this object is held per file AND
|
|
23
|
+
* grid — so a file cut two ways kept two copies of one immutable list. They live
|
|
24
|
+
* on the source file now, which is one object per file whatever grids are cut
|
|
25
|
+
* from it. What is left here is the two things that really are per grid: where
|
|
26
|
+
* the cuts are, and what the player was told they are.
|
|
27
|
+
*
|
|
28
|
+
* **Two tables, and they are not the same thing.** `boundaries` is where the
|
|
29
|
+
* file is cut NOW, corrections included, and it is what a run is told to cut
|
|
30
|
+
* at. `published` is what the player was given, written once and never changed,
|
|
31
|
+
* and it is what a segment must be stamped to. A player places a fragment by
|
|
32
|
+
* the playlist it holds; the live table keeps moving as produced segments
|
|
33
|
+
* reveal where the file's cuts really are.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A fresh tally of how well a container's keyframe index matches its file.
|
|
38
|
+
*
|
|
39
|
+
* @returns {{ checked: number, disagreed: number, maxDeviationSec: number, firstDisagreementIndex: number, deviations: number[], landedOnAnotherKeyframe: number, seen: Set<number> }}
|
|
40
|
+
*/
|
|
41
|
+
export function newIndexCheck() {
|
|
42
|
+
return {
|
|
43
|
+
checked: 0,
|
|
44
|
+
disagreed: 0,
|
|
45
|
+
maxDeviationSec: 0,
|
|
46
|
+
firstDisagreementIndex: -1,
|
|
47
|
+
// Every deviation, so the summary can report a distribution instead of one
|
|
48
|
+
// extreme. Bounded by the number of distinct boundaries a file produces.
|
|
49
|
+
deviations: [],
|
|
50
|
+
// Of the segments that started away from the playlist, how many began at
|
|
51
|
+
// ANOTHER time in the very list the grid was built from. This is the
|
|
52
|
+
// measurement that separates the two explanations: a table that describes
|
|
53
|
+
// times the file does not have, against a table that lists only SOME
|
|
54
|
+
// keyframes and a grid built over its gaps. Asked 2026-08-17 by the user,
|
|
55
|
+
// who was right that the second is far more likely — every deviation
|
|
56
|
+
// measured that day was positive, 0.58-2.96 s, which is what a cut pushed
|
|
57
|
+
// forward to the next real keyframe looks like.
|
|
58
|
+
landedOnAnotherKeyframe: 0,
|
|
59
|
+
// Which boundaries have been counted. A segment can be requested again, and
|
|
60
|
+
// a repeat is the same boundary, not new evidence.
|
|
61
|
+
seen: new Set()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class Timeline {
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} params
|
|
68
|
+
* @param {number[]} params.boundaries - Cut times in seconds, ascending, one
|
|
69
|
+
* more than there are segments.
|
|
70
|
+
* @param {number[] | null} [params.published] - What the player was told, when
|
|
71
|
+
* that differs from the boundaries because corrections have been made since.
|
|
72
|
+
* @param {"keyframe" | "uniform"} params.cutGrid - Whether those times are
|
|
73
|
+
* the source's own keyframes, which a copied picture has no choice about,
|
|
74
|
+
* or an even grid the encoder is told to place keyframes on.
|
|
75
|
+
* @param {number[] | null} [params.sourceTimes] - The same cuts on the FILE's
|
|
76
|
+
* own clock — the keyframe a run must seek to for each boundary.
|
|
77
|
+
*/
|
|
78
|
+
constructor({ boundaries, published = null, cutGrid, sourceTimes = null }) {
|
|
79
|
+
this.boundaries = Array.isArray(boundaries) ? boundaries : [];
|
|
80
|
+
// What the player holds. Taken from the boundaries as they stood when the
|
|
81
|
+
// playlist was written, and never touched again. Given outright only when a
|
|
82
|
+
// table is being restored with corrections already in it — a live one is
|
|
83
|
+
// always published from its own boundaries.
|
|
84
|
+
this.published = Array.isArray(published) ? published : [...this.boundaries];
|
|
85
|
+
this.cutGrid = cutGrid === "keyframe" ? "keyframe" : "uniform";
|
|
86
|
+
// Where each cut is on the FILE's clock, which is what a seek asks for.
|
|
87
|
+
// Frozen beside `published` and never corrected: a run must land where the
|
|
88
|
+
// player was told the segment begins, and the corrections belong to the
|
|
89
|
+
// live table. Empty when the grid was restored without it, and then the
|
|
90
|
+
// seek falls back to searching the file's keyframe list — which is the
|
|
91
|
+
// lossy path this exists to replace, kept only so a restored table still
|
|
92
|
+
// plays.
|
|
93
|
+
this.sourceTimes = Array.isArray(sourceTimes) && sourceTimes.length === this.published.length
|
|
94
|
+
? sourceTimes
|
|
95
|
+
: [];
|
|
96
|
+
// How well this container's keyframe index matches its own file. A fact
|
|
97
|
+
// about the FILE and its index: asked per session it would be answered a
|
|
98
|
+
// different number of times for one film depending on how many people
|
|
99
|
+
// happened to watch it.
|
|
100
|
+
this.indexCheck = newIndexCheck();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @returns {number} How many segments this file is cut into. */
|
|
104
|
+
get segmentCount() {
|
|
105
|
+
return Math.max(0, this.boundaries.length - 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Where segment `index` begins, on the timeline the player was given.
|
|
110
|
+
*
|
|
111
|
+
* @param {number} index
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
publishedStartOf(index) {
|
|
115
|
+
if (!Number.isInteger(index) || index <= 0) {
|
|
116
|
+
return this.published[0] ?? 0;
|
|
117
|
+
}
|
|
118
|
+
const at = Math.min(index, this.published.length - 1);
|
|
119
|
+
return this.published[at] ?? 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Where segment `index` begins on the FILE's own clock: the keyframe a run
|
|
124
|
+
* starting there must seek to.
|
|
125
|
+
*
|
|
126
|
+
* Not `publishedStartOf(index) + startTime`. That round trip is lossy, and a
|
|
127
|
+
* residue of two parts in a quadrillion costs a whole keyframe interval when
|
|
128
|
+
* the result is looked up in the container's list by value — which is how a
|
|
129
|
+
* run came to be numbered from #36 while carrying film 17.4 s earlier
|
|
130
|
+
* (2026-09-05). The number the container stated is carried, not rebuilt.
|
|
131
|
+
*
|
|
132
|
+
* @param {number} index
|
|
133
|
+
* @returns {number | null} Null when this table was restored without the
|
|
134
|
+
* source clock, and the caller must fall back to searching.
|
|
135
|
+
*/
|
|
136
|
+
sourceStartOf(index) {
|
|
137
|
+
if (this.sourceTimes.length === 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isInteger(index) || index <= 0) {
|
|
141
|
+
return this.sourceTimes[0] ?? null;
|
|
142
|
+
}
|
|
143
|
+
return this.sourceTimes[Math.min(index, this.sourceTimes.length - 1)] ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Where segment `index` begins on the live table — where a run cutting now
|
|
148
|
+
* will really put it.
|
|
149
|
+
*
|
|
150
|
+
* @param {number} index
|
|
151
|
+
* @returns {number}
|
|
152
|
+
*/
|
|
153
|
+
liveStartOf(index) {
|
|
154
|
+
if (!Number.isInteger(index) || index <= 0) {
|
|
155
|
+
return this.boundaries[0] ?? 0;
|
|
156
|
+
}
|
|
157
|
+
const at = Math.min(index, this.boundaries.length - 1);
|
|
158
|
+
return this.boundaries[at] ?? 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Which segment holds this moment.
|
|
163
|
+
*
|
|
164
|
+
* @param {number} seconds
|
|
165
|
+
* @returns {number}
|
|
166
|
+
*/
|
|
167
|
+
indexForTime(seconds) {
|
|
168
|
+
const wanted = Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
|
|
169
|
+
for (let index = 0; index < this.segmentCount; index += 1) {
|
|
170
|
+
if (wanted < this.boundaries[index + 1]) {
|
|
171
|
+
return index;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return Math.max(0, this.segmentCount - 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The timelines this proxy holds, one per file and grid.
|
|
181
|
+
*
|
|
182
|
+
* Keyed by the two things that decide where the cuts are: which file, and
|
|
183
|
+
* whether the cuts are its own keyframes or an even grid. A quality step of the
|
|
184
|
+
* same film is a different OUTPUT and the same timeline, which is exactly the
|
|
185
|
+
* agreement `inheritedGrid` used to arrange by copying.
|
|
186
|
+
*/
|
|
187
|
+
export class Timelines {
|
|
188
|
+
/** @type {Map<string, Timeline>} */
|
|
189
|
+
#byKey = new Map();
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {string} sourceKey
|
|
193
|
+
* @param {number} fileIndex
|
|
194
|
+
* @param {"keyframe" | "uniform"} cutGrid
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
static keyFor(sourceKey, fileIndex, cutGrid) {
|
|
198
|
+
return `${sourceKey}:${fileIndex}:${cutGrid === "keyframe" ? "kf" : "even"}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The one for this file and grid, made by `build` if it is not there yet.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} key
|
|
205
|
+
* @param {() => Timeline} build
|
|
206
|
+
* @returns {Timeline}
|
|
207
|
+
*/
|
|
208
|
+
get(key, build) {
|
|
209
|
+
let timeline = this.#byKey.get(key);
|
|
210
|
+
if (!timeline) {
|
|
211
|
+
timeline = build();
|
|
212
|
+
this.#byKey.set(key, timeline);
|
|
213
|
+
}
|
|
214
|
+
return timeline;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {string} key
|
|
219
|
+
* @returns {Timeline | null}
|
|
220
|
+
*/
|
|
221
|
+
peek(key) {
|
|
222
|
+
return this.#byKey.get(key) ?? null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @param {string} key */
|
|
226
|
+
forget(key) {
|
|
227
|
+
this.#byKey.delete(key);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Drop every timeline nobody is holding.
|
|
232
|
+
*
|
|
233
|
+
* A timeline is small — two arrays of a few thousand numbers — and it is kept
|
|
234
|
+
* for as long as somebody is reading the file it describes. Nothing else
|
|
235
|
+
* removes one, and a map that only ever grows is the shape of half the memory
|
|
236
|
+
* faults recorded in this project.
|
|
237
|
+
*
|
|
238
|
+
* @param {Set<Timeline>} inUse
|
|
239
|
+
* @returns {number} How many were dropped.
|
|
240
|
+
*/
|
|
241
|
+
forgetUnused(inUse) {
|
|
242
|
+
let dropped = 0;
|
|
243
|
+
for (const [key, timeline] of [...this.#byKey]) {
|
|
244
|
+
if (!inUse.has(timeline)) {
|
|
245
|
+
this.#byKey.delete(key);
|
|
246
|
+
dropped += 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return dropped;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** @returns {number} */
|
|
253
|
+
get size() {
|
|
254
|
+
return this.#byKey.size;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a file is cut, and which keyframe each cut is.
|
|
3
|
+
*
|
|
4
|
+
* Two answers, not one, and that is the whole point of this file existing.
|
|
5
|
+
*
|
|
6
|
+
* A cut has a time on the PLAYER's clock — 0-based, because a playlist is —
|
|
7
|
+
* and it has a time on the FILE's clock, which is the keyframe the muxer will
|
|
8
|
+
* actually seek to. The two differ by the container's own start time, and a
|
|
9
|
+
* copied picture cannot be cut anywhere but at a real keyframe, so both are
|
|
10
|
+
* needed and neither can be derived from the other after the fact.
|
|
11
|
+
*
|
|
12
|
+
* **Deriving one from the other after the fact is what broke a viewing on
|
|
13
|
+
* 2026-09-05.** The 0-based time was stored, rounded to six places, and the
|
|
14
|
+
* seek then added the start time back and looked the result up in the file's
|
|
15
|
+
* keyframe list by value. That round trip is lossy: a keyframe at 26.234 s in a
|
|
16
|
+
* container starting at 0.083 s comes back as 26.233999999999998, the lookup
|
|
17
|
+
* takes "the keyframe at or before" that, and answers the PREVIOUS one — 8.717 s
|
|
18
|
+
* earlier. Two parts in a quadrillion became one keyframe interval, that
|
|
19
|
+
* interval became a trim, the trim moved every cut of the run backwards by
|
|
20
|
+
* another interval, and the run's files were numbered from #36 while carrying
|
|
21
|
+
* film 17.4 s before what the playlist says #36 holds. The player's video buffer
|
|
22
|
+
* covered the playhead, its audio buffer had a 17.4 s hole across it, the
|
|
23
|
+
* intersection was empty, and the viewer waited two minutes and was told the
|
|
24
|
+
* proxy had sent no video.
|
|
25
|
+
*
|
|
26
|
+
* So the keyframe is carried, by index, from the one place that knows it.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} CutGrid
|
|
31
|
+
* @property {number[]} boundaries - Cut times on the player's clock, 0-based,
|
|
32
|
+
* ascending, one more than there are segments.
|
|
33
|
+
* @property {number[]} sourceTimes - The same cuts on the FILE's clock: for a
|
|
34
|
+
* keyframe grid, the keyframe itself, exactly as the container stated it.
|
|
35
|
+
* Same length as `boundaries`, so an index names both.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cut a file into segments.
|
|
40
|
+
*
|
|
41
|
+
* On a keyframe grid the cuts are the container's own keyframes, thinned so
|
|
42
|
+
* that no segment is much shorter than asked for; the first cut is the start of
|
|
43
|
+
* the file and the last is its end, neither of which need be a keyframe.
|
|
44
|
+
*
|
|
45
|
+
* On a uniform grid the cuts are multiples of the segment length, and the two
|
|
46
|
+
* clocks are the same clock: a re-encode places its own keyframes and owes the
|
|
47
|
+
* container's start time nothing.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} params
|
|
50
|
+
* @param {boolean} params.useKeyframeGrid
|
|
51
|
+
* @param {number} params.durationSeconds
|
|
52
|
+
* @param {number} params.segDur - The segment length asked for.
|
|
53
|
+
* @param {number[]} [params.keyframeTimes] - On the file's own clock.
|
|
54
|
+
* @param {number} [params.startTime] - The container's start time.
|
|
55
|
+
* @returns {CutGrid}
|
|
56
|
+
*/
|
|
57
|
+
export function computeCutGrid({ useKeyframeGrid, durationSeconds, segDur, keyframeTimes, startTime }) {
|
|
58
|
+
const total = Number.isFinite(durationSeconds) && durationSeconds > 0 ? durationSeconds : 0;
|
|
59
|
+
const step = Number.isFinite(segDur) && segDur > 0 ? segDur : 4;
|
|
60
|
+
const base = Number.isFinite(startTime) ? startTime : 0;
|
|
61
|
+
const uniform = () => {
|
|
62
|
+
const boundaries = [];
|
|
63
|
+
for (let t = 0; t < total - 0.001; t += step) {
|
|
64
|
+
boundaries.push(Number(t.toFixed(6)));
|
|
65
|
+
}
|
|
66
|
+
boundaries.push(total);
|
|
67
|
+
// One clock: nothing here is a keyframe of the source, so nothing is owed
|
|
68
|
+
// the container's start time either.
|
|
69
|
+
return { boundaries, sourceTimes: [...boundaries] };
|
|
70
|
+
};
|
|
71
|
+
if (!useKeyframeGrid || !Array.isArray(keyframeTimes) || keyframeTimes.length === 0 || total <= 0) {
|
|
72
|
+
return uniform();
|
|
73
|
+
}
|
|
74
|
+
const kept = keyframeTimes
|
|
75
|
+
.filter((time) => Number.isFinite(time))
|
|
76
|
+
.map((time) => ({ source: time, published: time - base }))
|
|
77
|
+
.filter((cut) => cut.published >= -0.001 && cut.published < total - 0.05)
|
|
78
|
+
.sort((left, right) => left.published - right.published);
|
|
79
|
+
// The first cut is the start of the file, whatever the container's own clock
|
|
80
|
+
// says that is; the last is its end. Neither is a keyframe, and a run never
|
|
81
|
+
// seeks to either — index 0 is served without a seek at all.
|
|
82
|
+
const boundaries = [0];
|
|
83
|
+
const sourceTimes = [base];
|
|
84
|
+
for (const cut of kept) {
|
|
85
|
+
if (cut.published >= boundaries[boundaries.length - 1] + step - 0.05) {
|
|
86
|
+
boundaries.push(Number(cut.published.toFixed(6)));
|
|
87
|
+
// NOT the rounded value with the base added back. This is the number the
|
|
88
|
+
// container stated, and it is what the seek must ask for.
|
|
89
|
+
sourceTimes.push(cut.source);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
boundaries.push(total);
|
|
93
|
+
sourceTimes.push(total + base);
|
|
94
|
+
// A degenerate index — one keyframe, or none inside the film — is no grid at
|
|
95
|
+
// all, and an even one serves better than a table with a single entry.
|
|
96
|
+
return boundaries.length >= 2 ? { boundaries, sourceTimes } : uniform();
|
|
97
|
+
}
|