@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.
@@ -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
- constructor({ boundaries, published = null, cutGrid }) {
77
- this.boundaries = Array.isArray(boundaries) ? boundaries : [];
78
- // What the player holds. Taken from the boundaries as they stood when the
79
- // playlist was written, and never touched again. Given outright only when a
80
- // table is being restored with corrections already in it a live one is
81
- // always published from its own boundaries.
82
- this.published = Array.isArray(published) ? published : [...this.boundaries];
83
- this.cutGrid = cutGrid === "keyframe" ? "keyframe" : "uniform";
84
- // How well this container's keyframe index matches its own file. A fact
85
- // about the FILE and its index: asked per session it would be answered a
86
- // different number of times for one film depending on how many people
87
- // happened to watch it.
88
- this.indexCheck = newIndexCheck();
89
- }
90
-
91
- /** @returns {number} How many segments this file is cut into. */
92
- get segmentCount() {
93
- return Math.max(0, this.boundaries.length - 1);
94
- }
95
-
96
- /**
97
- * Where segment `index` begins, on the timeline the player was given.
98
- *
99
- * @param {number} index
100
- * @returns {number}
101
- */
102
- publishedStartOf(index) {
103
- if (!Number.isInteger(index) || index <= 0) {
104
- return this.published[0] ?? 0;
105
- }
106
- const at = Math.min(index, this.published.length - 1);
107
- return this.published[at] ?? 0;
108
- }
109
-
110
- /**
111
- * Where segment `index` begins on the live table — where a run cutting now
112
- * will really put it.
113
- *
114
- * @param {number} index
115
- * @returns {number}
116
- */
117
- liveStartOf(index) {
118
- if (!Number.isInteger(index) || index <= 0) {
119
- return this.boundaries[0] ?? 0;
120
- }
121
- const at = Math.min(index, this.boundaries.length - 1);
122
- return this.boundaries[at] ?? 0;
123
- }
124
-
125
- /**
126
- * Which segment holds this moment.
127
- *
128
- * @param {number} seconds
129
- * @returns {number}
130
- */
131
- indexForTime(seconds) {
132
- const wanted = Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
133
- for (let index = 0; index < this.segmentCount; index += 1) {
134
- if (wanted < this.boundaries[index + 1]) {
135
- return index;
136
- }
137
- }
138
- return Math.max(0, this.segmentCount - 1);
139
- }
140
-
141
- }
142
-
143
- /**
144
- * The timelines this proxy holds, one per file and grid.
145
- *
146
- * Keyed by the two things that decide where the cuts are: which file, and
147
- * whether the cuts are its own keyframes or an even grid. A quality step of the
148
- * same film is a different OUTPUT and the same timeline, which is exactly the
149
- * agreement `inheritedGrid` used to arrange by copying.
150
- */
151
- export class Timelines {
152
- /** @type {Map<string, Timeline>} */
153
- #byKey = new Map();
154
-
155
- /**
156
- * @param {string} sourceKey
157
- * @param {number} fileIndex
158
- * @param {"keyframe" | "uniform"} cutGrid
159
- * @returns {string}
160
- */
161
- static keyFor(sourceKey, fileIndex, cutGrid) {
162
- return `${sourceKey}:${fileIndex}:${cutGrid === "keyframe" ? "kf" : "even"}`;
163
- }
164
-
165
- /**
166
- * The one for this file and grid, made by `build` if it is not there yet.
167
- *
168
- * @param {string} key
169
- * @param {() => Timeline} build
170
- * @returns {Timeline}
171
- */
172
- get(key, build) {
173
- let timeline = this.#byKey.get(key);
174
- if (!timeline) {
175
- timeline = build();
176
- this.#byKey.set(key, timeline);
177
- }
178
- return timeline;
179
- }
180
-
181
- /**
182
- * @param {string} key
183
- * @returns {Timeline | null}
184
- */
185
- peek(key) {
186
- return this.#byKey.get(key) ?? null;
187
- }
188
-
189
- /** @param {string} key */
190
- forget(key) {
191
- this.#byKey.delete(key);
192
- }
193
-
194
- /**
195
- * Drop every timeline nobody is holding.
196
- *
197
- * A timeline is small — two arrays of a few thousand numbers — and it is kept
198
- * for as long as somebody is reading the file it describes. Nothing else
199
- * removes one, and a map that only ever grows is the shape of half the memory
200
- * faults recorded in this project.
201
- *
202
- * @param {Set<Timeline>} inUse
203
- * @returns {number} How many were dropped.
204
- */
205
- forgetUnused(inUse) {
206
- let dropped = 0;
207
- for (const [key, timeline] of [...this.#byKey]) {
208
- if (!inUse.has(timeline)) {
209
- this.#byKey.delete(key);
210
- dropped += 1;
211
- }
212
- }
213
- return dropped;
214
- }
215
-
216
- /** @returns {number} */
217
- get size() {
218
- return this.#byKey.size;
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
+ }