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,542 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import type SegmentSinksStore from "../../segment_sinks";
3
+ import type { IBufferedChunk } from "../../segment_sinks";
4
+ import FreezeResolver from "../FreezeResolver";
5
+ import type { IFreezeResolverObservation } from "../FreezeResolver";
6
+
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
9
+ /* eslint-disable @typescript-eslint/unbound-method */
10
+
11
+ const {
12
+ mockGetCurrent,
13
+ mockLogInfo,
14
+ mockLogWarn,
15
+ mockLogDebug,
16
+ mockGetMonotonicTimeStamp,
17
+ } = vi.hoisted(() => ({
18
+ mockGetCurrent: vi.fn(() => ({
19
+ UNFREEZING_SEEK_DELAY: 3000,
20
+ UNFREEZING_DELTA_POSITION: 0.001,
21
+ FREEZING_FLUSH_FAILURE_DELAY: {
22
+ MINIMUM: 1000,
23
+ MAXIMUM: 10000,
24
+ POSITION_DELTA: 0.1,
25
+ },
26
+ })),
27
+ mockLogInfo: vi.fn(),
28
+ mockLogWarn: vi.fn(),
29
+ mockLogDebug: vi.fn(),
30
+ mockGetMonotonicTimeStamp: vi.fn(),
31
+ }));
32
+
33
+ vi.mock("../../../config", () => ({
34
+ default: {
35
+ getCurrent: mockGetCurrent,
36
+ },
37
+ }));
38
+
39
+ vi.mock("../../../log", () => ({
40
+ default: {
41
+ info: mockLogInfo,
42
+ warn: mockLogWarn,
43
+ debug: mockLogDebug,
44
+ },
45
+ }));
46
+
47
+ vi.mock("../../../utils/monotonic_timestamp", () => ({
48
+ default: mockGetMonotonicTimeStamp,
49
+ }));
50
+
51
+ describe("FreezeResolver", () => {
52
+ let freezeResolver: FreezeResolver;
53
+ let mockSegmentSinksStore: SegmentSinksStore;
54
+ let mockTimestamp: number;
55
+
56
+ const createMockObservation = (
57
+ overrides: Partial<IFreezeResolverObservation> = {},
58
+ ): IFreezeResolverObservation => ({
59
+ readyState: 4,
60
+ rebuffering: null,
61
+ freezing: null,
62
+ bufferGap: 10,
63
+ position: {
64
+ getPolled: () => 100,
65
+ } as any,
66
+ fullyLoaded: false,
67
+ ...overrides,
68
+ });
69
+
70
+ const createMockSegmentSinksStore = (): SegmentSinksStore =>
71
+ ({
72
+ getStatus: vi.fn(() => ({
73
+ type: "initialized",
74
+ value: {
75
+ getLastKnownInventory: vi.fn(() => []),
76
+ },
77
+ })),
78
+ }) as any;
79
+
80
+ beforeEach(() => {
81
+ mockTimestamp = 10000;
82
+ mockGetMonotonicTimeStamp.mockImplementation(() => mockTimestamp);
83
+ mockSegmentSinksStore = createMockSegmentSinksStore();
84
+ freezeResolver = new FreezeResolver(mockSegmentSinksStore);
85
+ });
86
+
87
+ afterEach(() => {
88
+ vi.clearAllMocks();
89
+ });
90
+
91
+ describe("onNewObservation - No freeze scenarios", () => {
92
+ it("should return null when playback is not frozen", () => {
93
+ const observation = createMockObservation({
94
+ readyState: 4,
95
+ freezing: null,
96
+ rebuffering: null,
97
+ });
98
+ const result = freezeResolver.onNewObservation(observation);
99
+ expect(result).toBeNull();
100
+ });
101
+
102
+ it("should return null when readyState is 1 but buffer gap is too small", () => {
103
+ const observation = createMockObservation({
104
+ readyState: 1,
105
+ bufferGap: 3, // Less than MINIMUM_BUFFER_GAP_AT_READY_STATE_1_BEFORE_FREEZING (6)
106
+ freezing: null,
107
+ rebuffering: null,
108
+ fullyLoaded: false,
109
+ });
110
+ const result = freezeResolver.onNewObservation(observation);
111
+ expect(result).toBeNull();
112
+ });
113
+
114
+ it("should ignore freeze when within ignore period", () => {
115
+ const observation1 = createMockObservation({
116
+ freezing: { timestamp: mockTimestamp },
117
+ });
118
+
119
+ // First observation triggers a flush
120
+ mockTimestamp = 10000;
121
+ freezeResolver.onNewObservation(observation1);
122
+ mockTimestamp = 13100; // Within UNFREEZING_SEEK_DELAY
123
+
124
+ const result = freezeResolver.onNewObservation(observation1);
125
+ expect(result).not.toBeNull();
126
+
127
+ // Second observation within ignore period
128
+ mockTimestamp = 13200; // Still within MINIMUM_TIME_BETWEEN_FREEZE_HANDLING (6000ms from flush)
129
+ const observation2 = createMockObservation({
130
+ freezing: { timestamp: 13200 },
131
+ });
132
+
133
+ const result2 = freezeResolver.onNewObservation(observation2);
134
+ expect(result2).toBeNull();
135
+ });
136
+ });
137
+
138
+ describe("onNewObservation - Freeze detection", () => {
139
+ it("should detect freeze when freezing status is set", () => {
140
+ const observation = createMockObservation({
141
+ freezing: { timestamp: mockTimestamp - 4000 },
142
+ bufferGap: 10,
143
+ });
144
+
145
+ const result = freezeResolver.onNewObservation(observation);
146
+
147
+ expect(result).not.toBeNull();
148
+ expect(mockLogInfo).toHaveBeenCalledWith(
149
+ "Freeze",
150
+ "Freeze detected",
151
+ expect.any(Object),
152
+ );
153
+ });
154
+
155
+ it("should detect freeze when readyState is 1 with sufficient buffer", () => {
156
+ const observation = createMockObservation({
157
+ readyState: 1,
158
+ bufferGap: 8, // Greater than MINIMUM_BUFFER_GAP_AT_READY_STATE_1_BEFORE_FREEZING
159
+ rebuffering: {
160
+ timestamp: mockTimestamp - 4000,
161
+ reason: "not-ready",
162
+ position: undefined,
163
+ },
164
+ });
165
+
166
+ const result = freezeResolver.onNewObservation(observation);
167
+ expect(result).not.toBeNull();
168
+ });
169
+
170
+ it("should detect freeze when fully loaded with readyState 1", () => {
171
+ const observation = createMockObservation({
172
+ readyState: 1,
173
+ bufferGap: 2,
174
+ fullyLoaded: true,
175
+ rebuffering: {
176
+ timestamp: mockTimestamp - 4000,
177
+ reason: "not-ready",
178
+ position: undefined,
179
+ },
180
+ });
181
+
182
+ const result = freezeResolver.onNewObservation(observation);
183
+ expect(result).not.toBeNull();
184
+ });
185
+ });
186
+
187
+ describe("onNewObservation - Flush strategy", () => {
188
+ it("should return flush resolution after freeze delay", () => {
189
+ const freezeTimestamp = mockTimestamp - 4000;
190
+ const observation = createMockObservation({
191
+ freezing: { timestamp: freezeTimestamp },
192
+ position: { getPolled: () => 100 } as any,
193
+ });
194
+
195
+ const result = freezeResolver.onNewObservation(observation);
196
+
197
+ expect(result).toEqual({
198
+ type: "flush",
199
+ value: { relativeSeek: 0.001 },
200
+ });
201
+ expect(mockLogDebug).toHaveBeenCalledWith("Freeze", "Trying to flush to un-freeze");
202
+ });
203
+
204
+ it("should not flush before UNFREEZING_SEEK_DELAY has passed", () => {
205
+ const freezeTimestamp = mockTimestamp - 2000; // Less than UNFREEZING_SEEK_DELAY (3000)
206
+ const observation = createMockObservation({
207
+ freezing: { timestamp: freezeTimestamp },
208
+ });
209
+
210
+ const result = freezeResolver.onNewObservation(observation);
211
+
212
+ expect(result).toBeNull();
213
+ });
214
+ });
215
+
216
+ describe("onNewObservation - Decipherability checks", () => {
217
+ it("should reload when buffer has undecipherable data", () => {
218
+ const mockSegment: IBufferedChunk = {
219
+ infos: {
220
+ representation: {
221
+ decipherable: false,
222
+ uniqueId: "rep1",
223
+ } as any,
224
+ },
225
+ start: 90,
226
+ end: 110,
227
+ } as any;
228
+
229
+ mockSegmentSinksStore.getStatus = vi.fn(
230
+ () =>
231
+ ({
232
+ type: "initialized",
233
+ value: {
234
+ getLastKnownInventory: vi.fn(() => [mockSegment]),
235
+ },
236
+ }) as any,
237
+ );
238
+
239
+ const observation = createMockObservation({
240
+ rebuffering: {
241
+ timestamp: mockTimestamp - 5000,
242
+ reason: "not-ready",
243
+ position: undefined,
244
+ },
245
+ readyState: 1,
246
+ bufferGap: 8,
247
+ });
248
+
249
+ const result = freezeResolver.onNewObservation(observation);
250
+
251
+ expect(result).toEqual({
252
+ type: "reload",
253
+ value: null,
254
+ });
255
+ expect(mockLogWarn).toHaveBeenCalledWith(
256
+ "Freeze",
257
+ expect.stringContaining("undecipherable segments"),
258
+ );
259
+ });
260
+
261
+ it("should first flush when frozen with only decipherable encrypted data", () => {
262
+ const mockSegment: IBufferedChunk = {
263
+ infos: {
264
+ representation: {
265
+ decipherable: true,
266
+ uniqueId: "rep1",
267
+ } as any,
268
+ },
269
+ start: 90,
270
+ end: 110,
271
+ } as any;
272
+
273
+ mockSegmentSinksStore.getStatus = vi.fn(
274
+ () =>
275
+ ({
276
+ type: "initialized",
277
+ value: {
278
+ getLastKnownInventory: vi.fn(() => [mockSegment]),
279
+ },
280
+ }) as any,
281
+ );
282
+
283
+ const observation = createMockObservation({
284
+ rebuffering: {
285
+ timestamp: mockTimestamp - 5000,
286
+ reason: "not-ready",
287
+ position: undefined,
288
+ },
289
+ readyState: 1,
290
+ bufferGap: 8,
291
+ });
292
+
293
+ const result = freezeResolver.onNewObservation(observation);
294
+ expect(result).toEqual({
295
+ type: "flush",
296
+ value: {
297
+ relativeSeek: 0.001,
298
+ },
299
+ });
300
+ });
301
+ });
302
+
303
+ describe("onNewObservation - Failed flush handling", () => {
304
+ it("should reload when flush fails and no representation change detected", () => {
305
+ // First: trigger a flush
306
+ mockTimestamp = 10000;
307
+ const observation1 = createMockObservation({
308
+ freezing: { timestamp: 6000 },
309
+ position: { getPolled: () => 100 } as any,
310
+ });
311
+
312
+ const flushResult = freezeResolver.onNewObservation(observation1);
313
+ expect(flushResult?.type).toBe("flush");
314
+
315
+ // Second: still frozen after flush, need to wait beyond the ignore period
316
+ // MINIMUM_TIME_BETWEEN_FREEZE_HANDLING is 6000ms
317
+ mockTimestamp = 17500; // 7500ms after flush, beyond ignore period (16000 + 1500)
318
+ const observation2 = createMockObservation({
319
+ freezing: { timestamp: 17500 },
320
+ position: { getPolled: () => 100.001 } as any, // Very close to flush position
321
+ });
322
+
323
+ const result = freezeResolver.onNewObservation(observation2);
324
+
325
+ expect(result).toEqual({
326
+ type: "reload",
327
+ value: null,
328
+ });
329
+ });
330
+
331
+ it("should avoid representation when freeze happens on representation switch", () => {
332
+ const mockSegment1: IBufferedChunk = {
333
+ infos: {
334
+ representation: { uniqueId: "rep1", bitrate: 1000000 } as any,
335
+ adaptation: { id: "video-ada" } as any,
336
+ period: { id: "period1" } as any,
337
+ },
338
+ start: 90,
339
+ end: 100,
340
+ bufferedStart: 90,
341
+ bufferedEnd: 100,
342
+ } as any;
343
+
344
+ const mockSegment2: IBufferedChunk = {
345
+ infos: {
346
+ representation: { uniqueId: "rep2", bitrate: 2000000 } as any,
347
+ adaptation: { id: "video-ada" } as any,
348
+ period: { id: "period1" } as any,
349
+ },
350
+ start: 100,
351
+ end: 110,
352
+ bufferedStart: 100,
353
+ bufferedEnd: 110,
354
+ } as any;
355
+
356
+ // Build history with representation switch
357
+ mockTimestamp = 10000;
358
+ mockSegmentSinksStore.getStatus = vi.fn(
359
+ () =>
360
+ ({
361
+ type: "initialized",
362
+ value: {
363
+ getLastKnownInventory: vi.fn(() => [mockSegment1]),
364
+ },
365
+ }) as any,
366
+ );
367
+ freezeResolver.onNewObservation(
368
+ createMockObservation({ position: { getPolled: () => 95 } as any }),
369
+ );
370
+
371
+ mockTimestamp = 11000;
372
+ mockSegmentSinksStore.getStatus = vi.fn(
373
+ () =>
374
+ ({
375
+ type: "initialized",
376
+ value: {
377
+ getLastKnownInventory: vi.fn(() => [mockSegment2]),
378
+ },
379
+ }) as any,
380
+ );
381
+ freezeResolver.onNewObservation(
382
+ createMockObservation({ position: { getPolled: () => 105 } as any }),
383
+ );
384
+
385
+ // Trigger flush
386
+ mockTimestamp = 15000;
387
+ const observation1 = createMockObservation({
388
+ freezing: { timestamp: 11000 }, // Frozen for 4000ms
389
+ position: { getPolled: () => 105 } as any,
390
+ });
391
+ const flushResult = freezeResolver.onNewObservation(observation1);
392
+ expect(flushResult?.type).toBe("flush");
393
+
394
+ // Still frozen after flush, need to wait beyond ignore period (6000ms from flush at 15000 = 21000)
395
+ mockTimestamp = 22000;
396
+ const observation2 = createMockObservation({
397
+ freezing: { timestamp: 22000 },
398
+ position: { getPolled: () => 105.001 } as any,
399
+ });
400
+
401
+ const result = freezeResolver.onNewObservation(observation2);
402
+
403
+ expect(result).toEqual({
404
+ type: "avoid-representations",
405
+ value: expect.arrayContaining([
406
+ expect.objectContaining({
407
+ representation: expect.objectContaining({ uniqueId: "rep2" }),
408
+ }),
409
+ ]),
410
+ });
411
+ });
412
+
413
+ it("should reload when freeze happens on period switch", () => {
414
+ const mockSegment1: IBufferedChunk = {
415
+ infos: {
416
+ representation: { uniqueId: "rep1" } as any,
417
+ adaptation: { id: "video-ada" } as any,
418
+ period: { id: "period1" } as any,
419
+ },
420
+ start: 90,
421
+ end: 100,
422
+ bufferedStart: 90,
423
+ bufferedEnd: 100,
424
+ } as any;
425
+
426
+ const mockSegment2: IBufferedChunk = {
427
+ infos: {
428
+ representation: { uniqueId: "rep2" } as any,
429
+ adaptation: { id: "video-ada" } as any,
430
+ period: { id: "period2" } as any, // Different period
431
+ },
432
+ start: 100,
433
+ end: 110,
434
+ bufferedStart: 100,
435
+ bufferedEnd: 110,
436
+ } as any;
437
+
438
+ // Build history with period switch
439
+ mockTimestamp = 10000;
440
+ mockSegmentSinksStore.getStatus = vi.fn(
441
+ () =>
442
+ ({
443
+ type: "initialized",
444
+ value: {
445
+ getLastKnownInventory: vi.fn(() => [mockSegment1]),
446
+ },
447
+ }) as any,
448
+ );
449
+ freezeResolver.onNewObservation(
450
+ createMockObservation({ position: { getPolled: () => 95 } as any }),
451
+ );
452
+
453
+ mockTimestamp = 11000;
454
+ mockSegmentSinksStore.getStatus = vi.fn(
455
+ () =>
456
+ ({
457
+ type: "initialized",
458
+ value: {
459
+ getLastKnownInventory: vi.fn(() => [mockSegment2]),
460
+ },
461
+ }) as any,
462
+ );
463
+ freezeResolver.onNewObservation(
464
+ createMockObservation({ position: { getPolled: () => 105 } as any }),
465
+ );
466
+
467
+ // Trigger flush
468
+ mockTimestamp = 15000;
469
+ const flushResult = freezeResolver.onNewObservation(
470
+ createMockObservation({
471
+ freezing: { timestamp: 11000 }, // Frozen for 4000ms
472
+ position: { getPolled: () => 105 } as any,
473
+ }),
474
+ );
475
+ expect(flushResult?.type).toBe("flush");
476
+
477
+ // Still frozen after flush, need to wait beyond ignore period (6000ms from flush at 15000 = 21000)
478
+ mockTimestamp = 22000;
479
+ const result = freezeResolver.onNewObservation(
480
+ createMockObservation({
481
+ freezing: { timestamp: 22000 },
482
+ position: { getPolled: () => 105.001 } as any,
483
+ }),
484
+ );
485
+
486
+ expect(result).toEqual({
487
+ type: "reload",
488
+ value: null,
489
+ });
490
+ });
491
+ });
492
+
493
+ describe("History management", () => {
494
+ it("should maintain segment history for audio and video", () => {
495
+ const mockSegment: IBufferedChunk = {
496
+ infos: {
497
+ representation: { uniqueId: "rep1" } as any,
498
+ },
499
+ start: 90,
500
+ bufferedStart: 90,
501
+ end: 110,
502
+ bufferedEnd: 110,
503
+ } as any;
504
+
505
+ mockSegmentSinksStore.getStatus = vi.fn(
506
+ () =>
507
+ ({
508
+ type: "initialized",
509
+ value: {
510
+ getLastKnownInventory: vi.fn(() => [mockSegment]),
511
+ },
512
+ }) as any,
513
+ );
514
+
515
+ const observation = createMockObservation({
516
+ position: { getPolled: () => 100 } as any,
517
+ });
518
+
519
+ freezeResolver.onNewObservation(observation);
520
+
521
+ // Verify history is tracked (implicitly tested through representation avoidance)
522
+ expect(mockSegmentSinksStore.getStatus).toHaveBeenCalledWith("audio");
523
+ expect(mockSegmentSinksStore.getStatus).toHaveBeenCalledWith("video");
524
+ });
525
+
526
+ it("should limit history to 100 entries per track type", () => {
527
+ // Create many observations to exceed history limit
528
+ for (let i = 0; i < 150; i++) {
529
+ mockTimestamp += 100;
530
+ freezeResolver.onNewObservation(
531
+ createMockObservation({
532
+ position: { getPolled: () => i } as any,
533
+ }),
534
+ );
535
+ }
536
+
537
+ // The test passes if no errors are thrown
538
+ // History should be limited internally
539
+ expect(true).toBe(true);
540
+ });
541
+ });
542
+ });