rx-player 4.5.0-dev.2026012302 → 4.5.0-dev.2026021900

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/VERSION +1 -1
  3. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  4. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  5. package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  6. package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
  7. package/dist/commonjs/compat/should_seek_at_zero.d.ts +12 -0
  8. package/dist/commonjs/compat/should_seek_at_zero.d.ts.map +1 -0
  9. package/dist/commonjs/compat/should_seek_at_zero.js +23 -0
  10. package/dist/commonjs/core/entry/FreezeResolver.js +2 -2
  11. package/dist/commonjs/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  12. package/dist/commonjs/core/entry/content_time_boundaries_observer.js +8 -0
  13. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  14. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  15. package/dist/commonjs/core/stream/representation/encryption_data_notifier.js +107 -0
  16. package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
  17. package/dist/commonjs/core/stream/representation/representation_stream.js +12 -60
  18. package/dist/commonjs/core/stream/representation/utils/get_needed_segments.js +1 -1
  19. package/dist/commonjs/default_config.d.ts +5 -0
  20. package/dist/commonjs/default_config.d.ts.map +1 -1
  21. package/dist/commonjs/default_config.js +5 -0
  22. package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
  23. package/dist/commonjs/main_thread/api/public_api.js +27 -3
  24. package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  25. package/dist/commonjs/main_thread/init/directfile_content_initializer.js +23 -1
  26. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  27. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +3 -1
  28. package/dist/commonjs/manifest/classes/index.d.ts +2 -1
  29. package/dist/commonjs/manifest/classes/index.d.ts.map +1 -1
  30. package/dist/commonjs/manifest/classes/representation.d.ts +1 -1
  31. package/dist/commonjs/manifest/classes/representation.d.ts.map +1 -1
  32. package/dist/commonjs/manifest/classes/representation.js +2 -2
  33. package/dist/commonjs/manifest/classes/representation_index/types.d.ts +2 -2
  34. package/dist/commonjs/utils/base64.d.ts +15 -1
  35. package/dist/commonjs/utils/base64.d.ts.map +1 -1
  36. package/dist/commonjs/utils/base64.js +6 -0
  37. package/dist/commonjs/utils/ranges.d.ts +1 -1
  38. package/dist/commonjs/utils/ranges.js +1 -1
  39. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  40. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  41. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  42. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  43. package/dist/es2017/compat/should_seek_at_zero.d.ts +12 -0
  44. package/dist/es2017/compat/should_seek_at_zero.d.ts.map +1 -0
  45. package/dist/es2017/compat/should_seek_at_zero.js +20 -0
  46. package/dist/es2017/core/entry/FreezeResolver.js +1 -1
  47. package/dist/es2017/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  48. package/dist/es2017/core/entry/content_time_boundaries_observer.js +8 -0
  49. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  50. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  51. package/dist/es2017/core/stream/representation/encryption_data_notifier.js +80 -0
  52. package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
  53. package/dist/es2017/core/stream/representation/representation_stream.js +10 -38
  54. package/dist/es2017/core/stream/representation/utils/get_needed_segments.js +1 -1
  55. package/dist/es2017/default_config.d.ts +5 -0
  56. package/dist/es2017/default_config.d.ts.map +1 -1
  57. package/dist/es2017/default_config.js +5 -0
  58. package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
  59. package/dist/es2017/main_thread/api/public_api.js +27 -3
  60. package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  61. package/dist/es2017/main_thread/init/directfile_content_initializer.js +23 -1
  62. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  63. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +3 -1
  64. package/dist/es2017/manifest/classes/index.d.ts +2 -1
  65. package/dist/es2017/manifest/classes/index.d.ts.map +1 -1
  66. package/dist/es2017/manifest/classes/representation.d.ts +1 -1
  67. package/dist/es2017/manifest/classes/representation.d.ts.map +1 -1
  68. package/dist/es2017/manifest/classes/representation.js +1 -1
  69. package/dist/es2017/manifest/classes/representation_index/types.d.ts +2 -2
  70. package/dist/es2017/utils/base64.d.ts +15 -1
  71. package/dist/es2017/utils/base64.d.ts.map +1 -1
  72. package/dist/es2017/utils/base64.js +6 -0
  73. package/dist/es2017/utils/ranges.d.ts +1 -1
  74. package/dist/es2017/utils/ranges.js +1 -1
  75. package/dist/mpd-parser.wasm +0 -0
  76. package/dist/worker.js +6 -6
  77. package/package.json +9 -3
  78. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  79. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  80. package/src/compat/should_seek_at_zero.ts +23 -0
  81. package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +790 -0
  82. package/src/core/adaptive/__tests__/network_analyzer.test.ts +514 -0
  83. package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +285 -0
  84. package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +247 -0
  85. package/src/core/entry/FreezeResolver.ts +1 -1
  86. package/src/core/entry/__tests__/FreezeResolver.test.ts +542 -0
  87. package/src/core/entry/__tests__/content_time_boundaries_observer.test.ts +868 -0
  88. package/src/core/entry/content_time_boundaries_observer.ts +10 -0
  89. package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +592 -0
  90. package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +646 -0
  91. package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +531 -0
  92. package/src/core/stream/representation/encryption_data_notifier.ts +122 -0
  93. package/src/core/stream/representation/representation_stream.ts +11 -49
  94. package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +321 -0
  95. package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +513 -0
  96. package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +132 -0
  97. package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +209 -0
  98. package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +343 -0
  99. package/src/core/stream/representation/utils/get_needed_segments.ts +1 -1
  100. package/src/default_config.ts +6 -0
  101. package/src/main_thread/api/public_api.ts +32 -3
  102. package/src/main_thread/init/directfile_content_initializer.ts +26 -1
  103. package/src/main_thread/init/utils/initial_seek_and_play.ts +5 -1
  104. package/src/manifest/classes/index.ts +2 -0
  105. package/src/manifest/classes/representation.ts +2 -1
  106. package/src/manifest/classes/representation_index/types.ts +2 -2
  107. package/src/utils/base64.ts +30 -1
  108. package/src/utils/ranges.ts +1 -1
@@ -0,0 +1,790 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import type { IRepresentation } from "../../../manifest";
3
+ import GuessBasedChooser from "../guess_based_chooser";
4
+ import { ABRAlgorithmType } from "../utils/last_estimate_storage";
5
+ import type { IRequestInfo } from "../utils/pending_requests_store";
6
+ import type { IRepresentationMaintainabilityScore } from "../utils/representation_score_calculator";
7
+ import { ScoreConfidenceLevel } from "../utils/representation_score_calculator";
8
+
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
11
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
12
+
13
+ const mocks = vi.hoisted(() => ({
14
+ estimateRequestBandwidth: vi.fn(),
15
+ getMonotonicTimeStamp: vi.fn(),
16
+ }));
17
+
18
+ vi.mock("../../../log", () => ({
19
+ default: {
20
+ debug: vi.fn(),
21
+ error: vi.fn(),
22
+ },
23
+ }));
24
+
25
+ vi.mock("../../../utils/array_find_index", () => ({
26
+ // eslint-disable-next-line no-restricted-properties
27
+ default: vi.fn((arr, predicate) => arr.findIndex(predicate)),
28
+ }));
29
+
30
+ vi.mock("../../../utils/monotonic_timestamp", () => ({
31
+ default: mocks.getMonotonicTimeStamp,
32
+ }));
33
+
34
+ vi.mock("../network_analyzer", () => ({
35
+ estimateRequestBandwidth: mocks.estimateRequestBandwidth,
36
+ }));
37
+
38
+ // Helper functions
39
+ function createRepresentation(id: string, bitrate: number): IRepresentation {
40
+ return {
41
+ id,
42
+ bitrate,
43
+ } as IRepresentation;
44
+ }
45
+
46
+ function createScoreData(
47
+ score: number,
48
+ confidenceLevel: ScoreConfidenceLevel,
49
+ ): IRepresentationMaintainabilityScore {
50
+ return { score, confidenceLevel };
51
+ }
52
+
53
+ function createRequest(
54
+ representationId: string,
55
+ segmentDuration: number,
56
+ requestTimestamp: number,
57
+ isInit = false,
58
+ ): IRequestInfo {
59
+ return {
60
+ content: {
61
+ representation: { id: representationId } as IRepresentation,
62
+ segment: {
63
+ duration: segmentDuration,
64
+ isInit,
65
+ },
66
+ },
67
+ requestTimestamp,
68
+ } as IRequestInfo;
69
+ }
70
+
71
+ describe("GuessBasedChooser", () => {
72
+ let mockScoreCalculator: any;
73
+ let mockPrevEstimate: any;
74
+ let representations: IRepresentation[];
75
+
76
+ beforeEach(() => {
77
+ vi.clearAllMocks();
78
+ mocks.getMonotonicTimeStamp.mockReturnValue(10000);
79
+
80
+ // Setup mock representations (sorted by bitrate ascending)
81
+ representations = [
82
+ createRepresentation("rep-1", 100000),
83
+ createRepresentation("rep-2", 500000),
84
+ createRepresentation("rep-3", 1000000),
85
+ createRepresentation("rep-4", 2000000),
86
+ createRepresentation("rep-5", 3000000),
87
+ ];
88
+
89
+ mockScoreCalculator = {
90
+ getEstimate: vi.fn(),
91
+ };
92
+
93
+ mockPrevEstimate = {
94
+ representation: null,
95
+ algorithmType: ABRAlgorithmType.GuessBased,
96
+ };
97
+ });
98
+
99
+ describe("constructor", () => {
100
+ it("should initialize with correct default values", () => {
101
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
102
+ expect(chooser).toBeDefined();
103
+ });
104
+ });
105
+
106
+ describe("getGuess", () => {
107
+ describe("when last chosen representation is null", () => {
108
+ it("should return null - nothing to base guess on", () => {
109
+ mockPrevEstimate.representation = null;
110
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
111
+
112
+ const result = chooser.getGuess(
113
+ representations,
114
+ { bufferGap: 5, speed: 1 },
115
+ representations[2],
116
+ 1500000,
117
+ [],
118
+ );
119
+
120
+ expect(result).toBeNull();
121
+ });
122
+ });
123
+
124
+ describe("when incomingBestBitrate > lastChosenRep.bitrate", () => {
125
+ it("should return null - ABR estimates are already superior", () => {
126
+ mockPrevEstimate.representation = representations[1]; // 500000
127
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.GuessBased;
128
+
129
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
130
+
131
+ const result = chooser.getGuess(
132
+ representations,
133
+ { bufferGap: 5, speed: 1 },
134
+ representations[1],
135
+ 1000000, // Higher than last chosen
136
+ [],
137
+ );
138
+
139
+ expect(result).toBeNull();
140
+ });
141
+
142
+ it("should reset consecutive wrong guesses when algorithm was GuessBased", () => {
143
+ mockPrevEstimate.representation = representations[1];
144
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.GuessBased;
145
+
146
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
147
+
148
+ // First call should reset counter
149
+ chooser.getGuess(
150
+ representations,
151
+ { bufferGap: 5, speed: 1 },
152
+ representations[1],
153
+ 1000000,
154
+ [],
155
+ );
156
+
157
+ // Subsequent calls should still work (counter was reset)
158
+ const result = chooser.getGuess(
159
+ representations,
160
+ { bufferGap: 5, speed: 1 },
161
+ representations[1],
162
+ 1000000,
163
+ [],
164
+ );
165
+
166
+ expect(result).toBeNull();
167
+ });
168
+ });
169
+
170
+ describe("when not in guessing mode yet", () => {
171
+ beforeEach(() => {
172
+ mockPrevEstimate.representation = representations[1];
173
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.BufferBased;
174
+ });
175
+
176
+ it("should return null when score data is undefined", () => {
177
+ mockScoreCalculator.getEstimate.mockReturnValue(undefined);
178
+
179
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
180
+
181
+ const result = chooser.getGuess(
182
+ representations,
183
+ { bufferGap: 5, speed: 1 },
184
+ representations[1],
185
+ 100000,
186
+ [],
187
+ );
188
+
189
+ expect(result).toBeNull();
190
+ });
191
+
192
+ it("should return next representation when conditions allow guessing higher", () => {
193
+ mockScoreCalculator.getEstimate.mockReturnValue(
194
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
195
+ );
196
+
197
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
198
+
199
+ const result = chooser.getGuess(
200
+ representations,
201
+ { bufferGap: 3, speed: 1 },
202
+ representations[1],
203
+ 100000,
204
+ [],
205
+ );
206
+
207
+ expect(result).toBe(representations[2]);
208
+ });
209
+
210
+ it("should return null when buffer gap is too low to guess higher", () => {
211
+ mockScoreCalculator.getEstimate.mockReturnValue(
212
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
213
+ );
214
+
215
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
216
+
217
+ const result = chooser.getGuess(
218
+ representations,
219
+ { bufferGap: 1.5, speed: 1 }, // Below 2.5 threshold
220
+ representations[1],
221
+ 100000,
222
+ [],
223
+ );
224
+
225
+ expect(result).toBeNull();
226
+ });
227
+
228
+ it("should return null when confidence level is not HIGH", () => {
229
+ mockScoreCalculator.getEstimate.mockReturnValue(
230
+ createScoreData(1.5, ScoreConfidenceLevel.LOW),
231
+ );
232
+
233
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
234
+
235
+ const result = chooser.getGuess(
236
+ representations,
237
+ { bufferGap: 3, speed: 1 },
238
+ representations[1],
239
+ 100000,
240
+ [],
241
+ );
242
+
243
+ expect(result).toBeNull();
244
+ });
245
+
246
+ it("should return null when score/speed ratio is too low", () => {
247
+ mockScoreCalculator.getEstimate.mockReturnValue(
248
+ createScoreData(1.005, ScoreConfidenceLevel.HIGH),
249
+ );
250
+
251
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
252
+
253
+ const result = chooser.getGuess(
254
+ representations,
255
+ { bufferGap: 3, speed: 1 },
256
+ representations[1],
257
+ 100000,
258
+ [],
259
+ );
260
+
261
+ expect(result).toBeNull();
262
+ });
263
+
264
+ it("should account for playback speed when checking score", () => {
265
+ mockScoreCalculator.getEstimate.mockReturnValue(
266
+ createScoreData(2.0, ScoreConfidenceLevel.HIGH),
267
+ );
268
+
269
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
270
+
271
+ const result = chooser.getGuess(
272
+ representations,
273
+ { bufferGap: 3, speed: 2.0 }, // Speed 2x requires higher score
274
+ representations[1],
275
+ 100000,
276
+ [],
277
+ );
278
+
279
+ expect(result).toBeNull(); // 2.0 / 2.0 = 1.0, not > 1.01
280
+ });
281
+
282
+ it("should return null when already at highest representation", () => {
283
+ mockScoreCalculator.getEstimate.mockReturnValue(
284
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
285
+ );
286
+
287
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
288
+
289
+ const result = chooser.getGuess(
290
+ representations,
291
+ { bufferGap: 3, speed: 1 },
292
+ representations[4], // Highest representation
293
+ 100000,
294
+ [],
295
+ );
296
+
297
+ expect(result).toBeNull();
298
+ });
299
+ });
300
+
301
+ describe("when already in guessing mode", () => {
302
+ beforeEach(() => {
303
+ mockPrevEstimate.representation = representations[2]; // 1000000
304
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.GuessBased;
305
+ });
306
+
307
+ it("should take higher guess when conditions are met", () => {
308
+ mockScoreCalculator.getEstimate.mockReturnValue(
309
+ createScoreData(2.0, ScoreConfidenceLevel.HIGH),
310
+ );
311
+
312
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
313
+
314
+ const result = chooser.getGuess(
315
+ representations,
316
+ { bufferGap: 3, speed: 1 },
317
+ representations[2],
318
+ 1000000,
319
+ [],
320
+ );
321
+ expect(result).toBe(representations[3]);
322
+ });
323
+
324
+ it("should take same guess when conditions are met with highest guess", () => {
325
+ mockPrevEstimate.representation = representations[4];
326
+ mockScoreCalculator.getEstimate.mockReturnValue(
327
+ createScoreData(2.0, ScoreConfidenceLevel.HIGH),
328
+ );
329
+
330
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
331
+
332
+ const result = chooser.getGuess(
333
+ representations,
334
+ { bufferGap: 3, speed: 1 },
335
+ representations[2],
336
+ 1000000,
337
+ [],
338
+ );
339
+ expect(result).toBe(representations[4]);
340
+ });
341
+
342
+ it("should return last chosen representation when current differs", () => {
343
+ mockScoreCalculator.getEstimate.mockReturnValue(
344
+ createScoreData(1.2, ScoreConfidenceLevel.HIGH),
345
+ );
346
+
347
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
348
+
349
+ const result = chooser.getGuess(
350
+ representations,
351
+ { bufferGap: 3, speed: 1 },
352
+ representations[1], // Different from last chosen
353
+ 100000,
354
+ [],
355
+ );
356
+
357
+ expect(result).toBe(representations[2]);
358
+ });
359
+
360
+ it("should stop guess and downgrade when score is too low", () => {
361
+ mockScoreCalculator.getEstimate.mockReturnValue(
362
+ createScoreData(0.9, ScoreConfidenceLevel.HIGH),
363
+ );
364
+
365
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
366
+
367
+ const result = chooser.getGuess(
368
+ representations,
369
+ { bufferGap: 3, speed: 1 },
370
+ representations[2],
371
+ 100000,
372
+ [],
373
+ );
374
+
375
+ expect(result).toBe(representations[1]); // Previous representation
376
+ });
377
+
378
+ it("should stop guess when buffer gap is low and score is marginal", () => {
379
+ mockScoreCalculator.getEstimate.mockReturnValue(
380
+ createScoreData(1.1, ScoreConfidenceLevel.HIGH),
381
+ );
382
+
383
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
384
+
385
+ const result = chooser.getGuess(
386
+ representations,
387
+ { bufferGap: 0.5, speed: 1 }, // Below 0.6 threshold
388
+ representations[2],
389
+ 100000,
390
+ [],
391
+ );
392
+
393
+ expect(result).toBe(representations[1]);
394
+ });
395
+
396
+ it("should stop guess when score is undefined and buffer gap is low", () => {
397
+ mockScoreCalculator.getEstimate.mockReturnValue(undefined);
398
+
399
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
400
+
401
+ const result = chooser.getGuess(
402
+ representations,
403
+ { bufferGap: 0.5, speed: 1 },
404
+ representations[2],
405
+ 100000,
406
+ [],
407
+ );
408
+
409
+ expect(result).toBe(representations[1]);
410
+ });
411
+
412
+ it("should block guesses for increasing duration after consecutive wrong guesses", () => {
413
+ mockScoreCalculator.getEstimate.mockReturnValue(
414
+ createScoreData(0.9, ScoreConfidenceLevel.HIGH),
415
+ );
416
+
417
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
418
+
419
+ // First wrong guess
420
+ chooser.getGuess(
421
+ representations,
422
+ { bufferGap: 3, speed: 1 },
423
+ representations[2],
424
+ 100000,
425
+ [],
426
+ );
427
+
428
+ // Try to guess again immediately
429
+ mocks.getMonotonicTimeStamp.mockReturnValue(10000 + 5000); // 5 seconds later
430
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.BufferBased;
431
+
432
+ const result = chooser.getGuess(
433
+ representations,
434
+ { bufferGap: 3, speed: 1 },
435
+ representations[1],
436
+ 100000,
437
+ [],
438
+ );
439
+
440
+ // Should be blocked (15 seconds block)
441
+ expect(result).toBeNull();
442
+ });
443
+
444
+ it("should allow guessing after block period expires", () => {
445
+ mockScoreCalculator.getEstimate.mockReturnValue(
446
+ createScoreData(0.9, ScoreConfidenceLevel.HIGH),
447
+ );
448
+
449
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
450
+
451
+ // First wrong guess
452
+ chooser.getGuess(
453
+ representations,
454
+ { bufferGap: 3, speed: 1 },
455
+ representations[2],
456
+ 100000,
457
+ [],
458
+ );
459
+
460
+ // Wait for block to expire
461
+ mocks.getMonotonicTimeStamp.mockReturnValue(10000 + 20000); // 20 seconds later
462
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.BufferBased;
463
+ mockScoreCalculator.getEstimate.mockReturnValue(
464
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
465
+ );
466
+
467
+ const result = chooser.getGuess(
468
+ representations,
469
+ { bufferGap: 3, speed: 1 },
470
+ representations[1],
471
+ 100000,
472
+ [],
473
+ );
474
+
475
+ // Should be allowed now
476
+ expect(result).toBe(representations[2]);
477
+ });
478
+
479
+ it("should guess higher when conditions allow in guessing mode", () => {
480
+ mockScoreCalculator.getEstimate.mockReturnValue(
481
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
482
+ );
483
+
484
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
485
+
486
+ const result = chooser.getGuess(
487
+ representations,
488
+ { bufferGap: 3, speed: 1 },
489
+ representations[2],
490
+ 100000,
491
+ [],
492
+ );
493
+
494
+ expect(result).toBe(representations[3]); // Next higher representation
495
+ });
496
+
497
+ it("should return current representation when score data is undefined and not stopping", () => {
498
+ mockScoreCalculator.getEstimate.mockReturnValue(undefined);
499
+
500
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
501
+
502
+ const result = chooser.getGuess(
503
+ representations,
504
+ { bufferGap: 3, speed: 1 },
505
+ representations[2],
506
+ 100000,
507
+ [],
508
+ );
509
+
510
+ expect(result).toBe(representations[2]);
511
+ });
512
+ });
513
+
514
+ describe("request-based guess stopping", () => {
515
+ beforeEach(() => {
516
+ mockPrevEstimate.representation = representations[2];
517
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.GuessBased;
518
+ mockScoreCalculator.getEstimate.mockReturnValue(
519
+ createScoreData(1.2, ScoreConfidenceLevel.HIGH),
520
+ );
521
+ });
522
+
523
+ it("should stop guess when init segment takes too long (>1000ms)", () => {
524
+ const requests = [
525
+ createRequest("rep-3", 0, 8000, true), // 2000ms elapsed (10000 - 8000)
526
+ ];
527
+
528
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
529
+
530
+ const result = chooser.getGuess(
531
+ representations,
532
+ { bufferGap: 3, speed: 1 },
533
+ representations[2],
534
+ 100000,
535
+ requests,
536
+ );
537
+
538
+ expect(result).toBe(representations[1]);
539
+ });
540
+
541
+ it("should not stop guess when init segment is fast enough", () => {
542
+ const requests = [
543
+ createRequest("rep-3", 0, 9500, true), // 500ms elapsed
544
+ ];
545
+
546
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
547
+
548
+ const result = chooser.getGuess(
549
+ representations,
550
+ { bufferGap: 3, speed: 1 },
551
+ representations[2],
552
+ 100000,
553
+ requests,
554
+ );
555
+
556
+ expect(result).toBe(representations[3]);
557
+ });
558
+
559
+ it("should stop guess when regular segment takes too long", () => {
560
+ const requests = [
561
+ createRequest("rep-3", 2, 5000, false), // 5000ms elapsed, duration is 2s
562
+ ];
563
+
564
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
565
+
566
+ const result = chooser.getGuess(
567
+ representations,
568
+ { bufferGap: 3, speed: 1 },
569
+ representations[2],
570
+ 100000,
571
+ requests,
572
+ );
573
+
574
+ expect(result).toBe(representations[1]); // Should downgrade
575
+ });
576
+
577
+ it("should not stop guess when regular segment is within acceptable time", () => {
578
+ const requests = [
579
+ createRequest("rep-3", 2, 8500, false), // 1500ms elapsed, duration is 2s (threshold: 2200ms)
580
+ ];
581
+
582
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
583
+
584
+ const result = chooser.getGuess(
585
+ representations,
586
+ { bufferGap: 3, speed: 1 },
587
+ representations[2],
588
+ 100000,
589
+ requests,
590
+ );
591
+
592
+ expect(result).toBe(representations[3]);
593
+ });
594
+
595
+ it("should stop guess when estimated bandwidth is too low", () => {
596
+ mocks.estimateRequestBandwidth.mockReturnValue(700000); // 70% of bitrate
597
+
598
+ const requests = [createRequest("rep-3", 2, 9000, false)];
599
+
600
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
601
+
602
+ const result = chooser.getGuess(
603
+ representations,
604
+ { bufferGap: 3, speed: 1 },
605
+ representations[2],
606
+ 100000,
607
+ requests,
608
+ );
609
+
610
+ expect(result).toBe(representations[1]);
611
+ });
612
+
613
+ it("should not consider requests for other representations", () => {
614
+ const requests = [createRequest("rep-4", 2, 5000, false)];
615
+
616
+ mockScoreCalculator.getEstimate.mockReturnValue(
617
+ createScoreData(1.1, ScoreConfidenceLevel.HIGH),
618
+ );
619
+
620
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
621
+
622
+ const result = chooser.getGuess(
623
+ representations,
624
+ { bufferGap: 2.0, speed: 1 },
625
+ representations[2],
626
+ 100000,
627
+ requests,
628
+ );
629
+
630
+ expect(result).toBe(representations[2]);
631
+ });
632
+
633
+ it("should handle multiple requests and stop on any failing condition", () => {
634
+ const requests = [
635
+ createRequest("rep-3", 2, 9500, false), // OK
636
+ createRequest("rep-3", 2, 5000, false), // TOO SLOW - should trigger stop
637
+ createRequest("rep-3", 2, 9000, false), // OK
638
+ ];
639
+
640
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
641
+
642
+ const result = chooser.getGuess(
643
+ representations,
644
+ { bufferGap: 3, speed: 1 },
645
+ representations[2],
646
+ 100000,
647
+ requests,
648
+ );
649
+
650
+ expect(result).toBe(representations[1]);
651
+ });
652
+ });
653
+
654
+ describe("guess validation", () => {
655
+ beforeEach(() => {
656
+ mockPrevEstimate.representation = representations[2];
657
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.GuessBased;
658
+ });
659
+
660
+ it("should validate guess with high score and high confidence", () => {
661
+ mockScoreCalculator.getEstimate.mockReturnValue(
662
+ createScoreData(1.6, ScoreConfidenceLevel.HIGH),
663
+ );
664
+
665
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
666
+
667
+ chooser.getGuess(
668
+ representations,
669
+ { bufferGap: 3, speed: 1 },
670
+ representations[2],
671
+ 100000,
672
+ [],
673
+ );
674
+
675
+ // If validated, should allow guessing higher next time
676
+ const result = chooser.getGuess(
677
+ representations,
678
+ { bufferGap: 3, speed: 1 },
679
+ representations[2],
680
+ 100000,
681
+ [],
682
+ );
683
+
684
+ expect(result).toBe(representations[3]);
685
+ });
686
+
687
+ it("should validate guess when incoming bitrate matches and it's an improvement", () => {
688
+ mockScoreCalculator.getEstimate.mockReturnValue(
689
+ createScoreData(1.2, ScoreConfidenceLevel.LOW),
690
+ );
691
+
692
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
693
+
694
+ const result = chooser.getGuess(
695
+ representations,
696
+ { bufferGap: 3, speed: 1 },
697
+ representations[2],
698
+ 1000000, // Matches current bitrate
699
+ [],
700
+ );
701
+
702
+ // Should be validated and allow progression
703
+ expect(result).toBe(representations[2]);
704
+ });
705
+ });
706
+
707
+ describe("edge cases", () => {
708
+ it("should handle infinite buffer gap correctly", () => {
709
+ mockPrevEstimate.representation = representations[1];
710
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.BufferBased;
711
+ mockScoreCalculator.getEstimate.mockReturnValue(
712
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
713
+ );
714
+
715
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
716
+
717
+ const result = chooser.getGuess(
718
+ representations,
719
+ { bufferGap: Infinity, speed: 1 },
720
+ representations[1],
721
+ 100000,
722
+ [],
723
+ );
724
+
725
+ expect(result).toBe(null);
726
+ });
727
+
728
+ it("should handle NaN buffer gap by not guessing", () => {
729
+ mockPrevEstimate.representation = representations[1];
730
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.BufferBased;
731
+ mockScoreCalculator.getEstimate.mockReturnValue(
732
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
733
+ );
734
+
735
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
736
+
737
+ const result = chooser.getGuess(
738
+ representations,
739
+ { bufferGap: NaN, speed: 1 },
740
+ representations[1],
741
+ 100000,
742
+ [],
743
+ );
744
+
745
+ expect(result).toBeNull();
746
+ });
747
+
748
+ it("should cap block duration at 120 seconds", () => {
749
+ mockScoreCalculator.getEstimate.mockReturnValue(
750
+ createScoreData(0.9, ScoreConfidenceLevel.HIGH),
751
+ );
752
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.GuessBased;
753
+ mockPrevEstimate.representation = representations[2];
754
+
755
+ const chooser = new GuessBasedChooser(mockScoreCalculator, mockPrevEstimate);
756
+
757
+ // Trigger many consecutive wrong guesses
758
+ for (let i = 0; i < 20; i++) {
759
+ chooser.getGuess(
760
+ representations,
761
+ { bufferGap: 3, speed: 1 },
762
+ representations[2],
763
+ 100000,
764
+ [],
765
+ );
766
+ mocks.getMonotonicTimeStamp.mockReturnValue(10000 + 1000 * (i + 1));
767
+ }
768
+
769
+ // The block should be capped at 120 seconds max
770
+ // After 120 seconds, should be able to guess again
771
+ mocks.getMonotonicTimeStamp.mockReturnValue(10000 + 20000 + 120100);
772
+ mockPrevEstimate.algorithmType = ABRAlgorithmType.BufferBased;
773
+ mockPrevEstimate.representation = representations[1]; // Update to reflect actual downgraded state
774
+ mockScoreCalculator.getEstimate.mockReturnValue(
775
+ createScoreData(1.5, ScoreConfidenceLevel.HIGH),
776
+ );
777
+
778
+ const result = chooser.getGuess(
779
+ representations,
780
+ { bufferGap: 3, speed: 1 },
781
+ representations[1],
782
+ 100000,
783
+ [],
784
+ );
785
+
786
+ expect(result).toBe(representations[2]);
787
+ });
788
+ });
789
+ });
790
+ });