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

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 (180) hide show
  1. package/CHANGELOG.md +21 -3
  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/cmcd/cmcd_data_builder.js +2 -2
  11. package/dist/commonjs/core/entry/FreezeResolver.js +2 -2
  12. package/dist/commonjs/core/entry/content_preparer.d.ts +1 -6
  13. package/dist/commonjs/core/entry/content_preparer.d.ts.map +1 -1
  14. package/dist/commonjs/core/entry/content_preparer.js +2 -125
  15. package/dist/commonjs/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  16. package/dist/commonjs/core/entry/content_time_boundaries_observer.js +8 -0
  17. package/dist/commonjs/core/entry/core_entry.d.ts +1 -1
  18. package/dist/commonjs/core/entry/core_entry.d.ts.map +1 -1
  19. package/dist/commonjs/core/entry/core_entry.js +2 -7
  20. package/dist/commonjs/core/entry/core_text_displayer_interface.d.ts.map +1 -1
  21. package/dist/commonjs/core/entry/core_text_displayer_interface.js +3 -1
  22. package/dist/commonjs/core/entry/index.d.ts +1 -1
  23. package/dist/commonjs/core/entry/index.d.ts.map +1 -1
  24. package/dist/commonjs/core/entry/utils.d.ts +64 -0
  25. package/dist/commonjs/core/entry/utils.d.ts.map +1 -1
  26. package/dist/commonjs/core/entry/utils.js +165 -0
  27. package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts +5 -6
  28. package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts.map +1 -1
  29. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
  30. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.js +34 -30
  31. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  32. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  33. package/dist/commonjs/core/stream/representation/encryption_data_notifier.js +107 -0
  34. package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
  35. package/dist/commonjs/core/stream/representation/representation_stream.js +12 -60
  36. package/dist/commonjs/core/stream/representation/utils/get_needed_segments.js +1 -1
  37. package/dist/commonjs/default_config.d.ts +5 -0
  38. package/dist/commonjs/default_config.d.ts.map +1 -1
  39. package/dist/commonjs/default_config.js +5 -0
  40. package/dist/commonjs/experimental/features/worker/index.d.ts +2 -1
  41. package/dist/commonjs/experimental/features/worker/index.d.ts.map +1 -1
  42. package/dist/commonjs/experimental/features/worker/index.js +4 -3
  43. package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
  44. package/dist/commonjs/main_thread/api/public_api.js +28 -4
  45. package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  46. package/dist/commonjs/main_thread/init/directfile_content_initializer.js +23 -1
  47. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  48. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +3 -1
  49. package/dist/commonjs/manifest/classes/index.d.ts +2 -1
  50. package/dist/commonjs/manifest/classes/index.d.ts.map +1 -1
  51. package/dist/commonjs/manifest/classes/representation.d.ts +1 -1
  52. package/dist/commonjs/manifest/classes/representation.d.ts.map +1 -1
  53. package/dist/commonjs/manifest/classes/representation.js +2 -2
  54. package/dist/commonjs/manifest/classes/representation_index/types.d.ts +2 -2
  55. package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
  56. package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
  57. package/dist/commonjs/utils/base64.d.ts +15 -1
  58. package/dist/commonjs/utils/base64.d.ts.map +1 -1
  59. package/dist/commonjs/utils/base64.js +6 -0
  60. package/dist/commonjs/utils/ranges.d.ts +1 -1
  61. package/dist/commonjs/utils/ranges.js +1 -1
  62. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  63. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  64. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  65. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  66. package/dist/es2017/compat/should_seek_at_zero.d.ts +12 -0
  67. package/dist/es2017/compat/should_seek_at_zero.d.ts.map +1 -0
  68. package/dist/es2017/compat/should_seek_at_zero.js +20 -0
  69. package/dist/es2017/core/cmcd/cmcd_data_builder.js +2 -2
  70. package/dist/es2017/core/entry/FreezeResolver.js +1 -1
  71. package/dist/es2017/core/entry/content_preparer.d.ts +1 -6
  72. package/dist/es2017/core/entry/content_preparer.d.ts.map +1 -1
  73. package/dist/es2017/core/entry/content_preparer.js +1 -76
  74. package/dist/es2017/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  75. package/dist/es2017/core/entry/content_time_boundaries_observer.js +8 -0
  76. package/dist/es2017/core/entry/core_entry.d.ts +1 -1
  77. package/dist/es2017/core/entry/core_entry.d.ts.map +1 -1
  78. package/dist/es2017/core/entry/core_entry.js +2 -7
  79. package/dist/es2017/core/entry/core_text_displayer_interface.d.ts.map +1 -1
  80. package/dist/es2017/core/entry/core_text_displayer_interface.js +3 -1
  81. package/dist/es2017/core/entry/index.d.ts +1 -1
  82. package/dist/es2017/core/entry/index.d.ts.map +1 -1
  83. package/dist/es2017/core/entry/utils.d.ts +64 -0
  84. package/dist/es2017/core/entry/utils.d.ts.map +1 -1
  85. package/dist/es2017/core/entry/utils.js +98 -0
  86. package/dist/es2017/core/fetchers/segment/segment_queue.d.ts +5 -6
  87. package/dist/es2017/core/fetchers/segment/segment_queue.d.ts.map +1 -1
  88. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
  89. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.js +21 -16
  90. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  91. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  92. package/dist/es2017/core/stream/representation/encryption_data_notifier.js +80 -0
  93. package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
  94. package/dist/es2017/core/stream/representation/representation_stream.js +10 -38
  95. package/dist/es2017/core/stream/representation/utils/get_needed_segments.js +1 -1
  96. package/dist/es2017/default_config.d.ts +5 -0
  97. package/dist/es2017/default_config.d.ts.map +1 -1
  98. package/dist/es2017/default_config.js +5 -0
  99. package/dist/es2017/experimental/features/worker/index.d.ts +2 -1
  100. package/dist/es2017/experimental/features/worker/index.d.ts.map +1 -1
  101. package/dist/es2017/experimental/features/worker/index.js +2 -3
  102. package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
  103. package/dist/es2017/main_thread/api/public_api.js +28 -4
  104. package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  105. package/dist/es2017/main_thread/init/directfile_content_initializer.js +23 -1
  106. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  107. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +3 -1
  108. package/dist/es2017/manifest/classes/index.d.ts +2 -1
  109. package/dist/es2017/manifest/classes/index.d.ts.map +1 -1
  110. package/dist/es2017/manifest/classes/representation.d.ts +1 -1
  111. package/dist/es2017/manifest/classes/representation.d.ts.map +1 -1
  112. package/dist/es2017/manifest/classes/representation.js +1 -1
  113. package/dist/es2017/manifest/classes/representation_index/types.d.ts +2 -2
  114. package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
  115. package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
  116. package/dist/es2017/utils/base64.d.ts +15 -1
  117. package/dist/es2017/utils/base64.d.ts.map +1 -1
  118. package/dist/es2017/utils/base64.js +6 -0
  119. package/dist/es2017/utils/ranges.d.ts +1 -1
  120. package/dist/es2017/utils/ranges.js +1 -1
  121. package/dist/mpd-parser.wasm +0 -0
  122. package/dist/worker.js +8 -8
  123. package/package.json +26 -23
  124. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  125. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  126. package/src/compat/should_seek_at_zero.ts +23 -0
  127. package/src/core/adaptive/__tests__/adaptive_representation_selector.test.ts +734 -0
  128. package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +793 -0
  129. package/src/core/adaptive/__tests__/network_analyzer.test.ts +514 -0
  130. package/src/core/adaptive/utils/__tests__/bandwidth_estimator.test.ts +117 -0
  131. package/src/core/adaptive/utils/__tests__/last_estimate_storage.test.ts +76 -0
  132. package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +288 -0
  133. package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +249 -0
  134. package/src/core/cmcd/__tests__/cmcd_data_builder.test.ts +464 -0
  135. package/src/core/cmcd/cmcd_data_builder.ts +2 -2
  136. package/src/core/entry/FreezeResolver.ts +1 -1
  137. package/src/core/entry/__tests__/FreezeResolver.test.ts +542 -0
  138. package/src/core/entry/__tests__/content_time_boundaries_observer.test.ts +867 -0
  139. package/src/core/entry/__tests__/core_text_displayer_interface.test.ts +205 -0
  140. package/src/core/entry/__tests__/get_buffered_data_per_media_buffer.test.ts +95 -0
  141. package/src/core/entry/__tests__/track_choice_setter.test.ts +288 -0
  142. package/src/core/entry/__tests__/utils.test.ts +309 -0
  143. package/src/core/entry/content_preparer.ts +7 -118
  144. package/src/core/entry/content_time_boundaries_observer.ts +10 -0
  145. package/src/core/entry/core_entry.ts +7 -14
  146. package/src/core/entry/core_text_displayer_interface.ts +3 -1
  147. package/src/core/entry/index.ts +1 -1
  148. package/src/core/entry/utils.ts +151 -0
  149. package/src/core/fetchers/__tests__/cdn_prioritizer.test.ts +166 -0
  150. package/src/core/fetchers/manifest/__tests__/manifest_fetcher.test.ts +803 -0
  151. package/src/core/fetchers/segment/__tests__/segment_fetcher.test.ts +872 -0
  152. package/src/core/fetchers/segment/__tests__/segment_queue.test.ts +557 -0
  153. package/src/core/fetchers/segment/segment_queue.ts +5 -6
  154. package/src/core/fetchers/thumbnails/__tests__/thumbnail_fetcher.test.ts +435 -0
  155. package/src/core/fetchers/thumbnails/thumbnail_fetcher.ts +28 -20
  156. package/src/core/fetchers/utils/__tests__/schedule_request.test.ts +331 -0
  157. package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +593 -0
  158. package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +646 -0
  159. package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +531 -0
  160. package/src/core/stream/representation/encryption_data_notifier.ts +122 -0
  161. package/src/core/stream/representation/representation_stream.ts +11 -49
  162. package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +321 -0
  163. package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +513 -0
  164. package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +132 -0
  165. package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +209 -0
  166. package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +343 -0
  167. package/src/core/stream/representation/utils/get_needed_segments.ts +1 -1
  168. package/src/default_config.ts +6 -0
  169. package/src/experimental/features/worker/index.ts +2 -4
  170. package/src/main_thread/api/public_api.ts +33 -4
  171. package/src/main_thread/init/directfile_content_initializer.ts +26 -1
  172. package/src/main_thread/init/utils/initial_seek_and_play.ts +5 -1
  173. package/src/manifest/classes/index.ts +2 -0
  174. package/src/manifest/classes/representation.ts +2 -1
  175. package/src/manifest/classes/representation_index/types.ts +2 -2
  176. package/src/mse/__tests__/main_media_source_interface.test.ts +846 -0
  177. package/src/parsers/texttracks/ttml/html/apply_line_height.ts +16 -1
  178. package/src/utils/__tests__/base64.test.ts +159 -191
  179. package/src/utils/base64.ts +30 -1
  180. package/src/utils/ranges.ts +1 -1
@@ -0,0 +1,872 @@
1
+ import type { Mock } from "vitest";
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
+ import type { ICdnMetadata } from "../../../../parsers/manifest";
4
+ import type {
5
+ ISegmentLoader,
6
+ ISegmentLoaderCallbacks,
7
+ ISegmentLoaderResultSegmentCreated,
8
+ ISegmentLoaderResultSegmentLoaded,
9
+ ISegmentParser,
10
+ } from "../../../../transports";
11
+ import sleep from "../../../../utils/sleep";
12
+ import TaskCanceller, { CancellationError } from "../../../../utils/task_canceller";
13
+ import type { CancellationSignal } from "../../../../utils/task_canceller";
14
+ import type {
15
+ IMetricsCallbackPayload,
16
+ IRequestBeginCallbackPayload,
17
+ IRequestEndCallbackPayload,
18
+ IRequestProgressCallbackPayload,
19
+ } from "../../../adaptive";
20
+ import type CmcdDataBuilder from "../../../cmcd";
21
+ import type CdnPrioritizer from "../../cdn_prioritizer";
22
+ import type { IBackoffSettings } from "../../utils/schedule_request";
23
+ import createSegmentFetcher, {
24
+ getSegmentFetcherRequestOptions,
25
+ } from "../segment_fetcher";
26
+ import type { ISegmentFetcherArguments, ISegmentLoaderContent } from "../segment_fetcher";
27
+ import type { IParsedInitSegmentPayload, IParsedSegmentPayload } from "../segment_queue";
28
+
29
+ /* eslint-disable @typescript-eslint/no-explicit-any */
30
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
31
+
32
+ const mockGetCurrent = vi.hoisted(() => vi.fn());
33
+ const mockFormatError = vi.hoisted(() => vi.fn((err: unknown) => err));
34
+ const mockLog = vi.hoisted(() => ({
35
+ debug: vi.fn(),
36
+ }));
37
+ const mockGetLoggableSegmentId = vi.hoisted(() => vi.fn(() => "seg-id"));
38
+ const mockGetTimestamp = vi.hoisted(() => vi.fn(() => 1000));
39
+ const mockScheduleRequestWithCdns = vi.hoisted(() => {
40
+ return vi.fn(
41
+ (
42
+ cdns: ICdnMetadata[] | null,
43
+ _cdnPrioritizer: CdnPrioritizer | null,
44
+ performRequest: (
45
+ cdn: ICdnMetadata | null,
46
+ cancellationSignal: CancellationSignal,
47
+ ) => Promise<ReturnType<ISegmentLoader<ArrayBuffer>>>,
48
+ _options: IBackoffSettings,
49
+ globalCancellationSignal: CancellationSignal,
50
+ ): Promise<ReturnType<ISegmentLoader<ArrayBuffer>>> => {
51
+ if (globalCancellationSignal.isCancelled()) {
52
+ return Promise.reject(globalCancellationSignal.cancellationError);
53
+ }
54
+ return performRequest(cdns?.[0] ?? null, new TaskCanceller("test task").signal);
55
+ },
56
+ );
57
+ });
58
+ const mockErrorSelector = vi.hoisted(() => vi.fn((err: unknown) => err));
59
+ const mockInitializationSegmentCacheGet = vi.hoisted(() => vi.fn((): unknown => null));
60
+ const mockInitializationSegmentCacheAdd = vi.hoisted(() => vi.fn());
61
+ const mockInitializationSegmentCache = vi.hoisted(
62
+ () =>
63
+ class {
64
+ get = mockInitializationSegmentCacheGet;
65
+ add = mockInitializationSegmentCacheAdd;
66
+ },
67
+ );
68
+
69
+ vi.mock("../../../../config", () => ({
70
+ default: { getCurrent: mockGetCurrent },
71
+ }));
72
+ vi.mock("../../../../errors", () => ({ formatError: mockFormatError }));
73
+ vi.mock("../../../../log", () => ({ default: mockLog }));
74
+ vi.mock("../../../../manifest", () => ({
75
+ getLoggableSegmentId: mockGetLoggableSegmentId,
76
+ }));
77
+ vi.mock("../../../../utils/monotonic_timestamp", () => ({ default: mockGetTimestamp }));
78
+ vi.mock("../../utils/schedule_request", () => ({
79
+ scheduleRequestWithCdns: mockScheduleRequestWithCdns,
80
+ }));
81
+ vi.mock("../../utils/error_selector", () => ({ default: mockErrorSelector }));
82
+ vi.mock("../initialization_segment_cache", () => ({
83
+ default: mockInitializationSegmentCache,
84
+ }));
85
+ vi.mock("../../../../utils/is_null_or_undefined", () => ({
86
+ default: (val: unknown) => val === null || val === undefined,
87
+ }));
88
+ vi.mock("../../../../utils/object_assign", () => ({
89
+ // eslint-disable-next-line no-restricted-properties
90
+ default: (target: object, ...sources: object[]) => Object.assign(target, ...sources),
91
+ }));
92
+
93
+ function makeContent(): ISegmentLoaderContent {
94
+ return {
95
+ manifest: { isLive: false, publishTime: 123 },
96
+ period: { start: 0, end: 100 },
97
+ adaptation: { type: "video", language: "fr" },
98
+ representation: {
99
+ mimeType: "video/mp4",
100
+ codecs: ["avc1"],
101
+ cdnMetadata: [{ url: "http://cdn1.test" }],
102
+ },
103
+ segment: { id: "seg1" },
104
+ nextSegment: undefined,
105
+ } as any;
106
+ }
107
+
108
+ function makeParsed(
109
+ type: "init" | "media" = "media",
110
+ duration = 2,
111
+ ): IParsedSegmentPayload<ArrayBuffer> | IParsedInitSegmentPayload<ArrayBuffer> {
112
+ if (type === "init") {
113
+ return {
114
+ segmentType: "init",
115
+ initializationData: new Uint8Array([5, 6, 8]).buffer,
116
+ protectionData: [],
117
+ initializationDataSize: 3,
118
+ segment: {} as any,
119
+ };
120
+ }
121
+ return {
122
+ segmentType: "media",
123
+ chunkInfos: {
124
+ duration,
125
+ time: 0,
126
+ },
127
+ appendWindow: [0, Number.MAX_SAFE_INTEGER],
128
+ protectionData: [],
129
+ chunkOffset: 0,
130
+ chunkSize: 3,
131
+ chunkData: new Uint8Array([7, 7, 7]).buffer,
132
+ segment: {} as any,
133
+ };
134
+ }
135
+
136
+ function makeLoadedResult(
137
+ responseData: ArrayBuffer = new Uint8Array([1, 2, 3]).buffer,
138
+ ): ISegmentLoaderResultSegmentLoaded<ArrayBuffer> {
139
+ return {
140
+ resultType: "segment-loaded",
141
+ resultData: { responseData, size: 3, requestDuration: 1500 },
142
+ };
143
+ }
144
+
145
+ function makeCreatedResult(
146
+ data: ArrayBuffer = new Uint8Array([4, 5, 6]).buffer,
147
+ ): ISegmentLoaderResultSegmentCreated<ArrayBuffer> {
148
+ return {
149
+ resultType: "segment-created",
150
+ resultData: data,
151
+ };
152
+ }
153
+
154
+ function makePipeline(
155
+ parsedResult:
156
+ | IParsedSegmentPayload<ArrayBuffer>
157
+ | IParsedInitSegmentPayload<ArrayBuffer> = makeParsed(),
158
+ loadResult:
159
+ | ISegmentLoaderResultSegmentLoaded<ArrayBuffer>
160
+ | ISegmentLoaderResultSegmentCreated<ArrayBuffer> = makeLoadedResult(),
161
+ ): {
162
+ loadSegment: Mock<ISegmentLoader<ArrayBuffer>>;
163
+ parseSegment: Mock<ISegmentParser<ArrayBuffer, ArrayBuffer>>;
164
+ } {
165
+ return {
166
+ loadSegment: vi.fn().mockResolvedValue(loadResult),
167
+ parseSegment: vi.fn().mockReturnValue(parsedResult),
168
+ };
169
+ }
170
+
171
+ function makeDefaultSegmentFetcherArgs(): ISegmentFetcherArguments<
172
+ ArrayBuffer,
173
+ ArrayBuffer
174
+ > & {
175
+ pipeline: {
176
+ loadSegment: Mock<ISegmentLoader<ArrayBuffer>>;
177
+ parseSegment: Mock<ISegmentParser<ArrayBuffer, ArrayBuffer>>;
178
+ };
179
+ eventListeners: {
180
+ onRequestBegin?: Mock<(arg: IRequestBeginCallbackPayload) => void>;
181
+ onProgress?: Mock<(arg: IRequestProgressCallbackPayload) => void>;
182
+ onRequestEnd?: Mock<(arg: IRequestEndCallbackPayload) => void>;
183
+ onMetrics?: Mock<(arg: IMetricsCallbackPayload) => void>;
184
+ };
185
+ } {
186
+ return {
187
+ bufferType: "video",
188
+ pipeline: makePipeline(),
189
+ cdnPrioritizer: null,
190
+ cmcdDataBuilder: null,
191
+ eventListeners: {
192
+ onRequestBegin: vi.fn(),
193
+ onProgress: vi.fn(),
194
+ onRequestEnd: vi.fn(),
195
+ onMetrics: vi.fn(),
196
+ },
197
+ requestOptions: {
198
+ baseDelay: 200,
199
+ maxDelay: 3000,
200
+ maxRetry: 3,
201
+ requestTimeout: 30000,
202
+ connectionTimeout: 5000,
203
+ },
204
+ };
205
+ }
206
+
207
+ describe("createSegmentFetcher", () => {
208
+ afterEach(() => {
209
+ vi.resetAllMocks();
210
+ });
211
+ describe("cache behavior", () => {
212
+ it("returns cached data without making a network request when cache hits", async () => {
213
+ const data = { a: 5 };
214
+ mockInitializationSegmentCacheGet.mockReturnValue(data);
215
+ const parsed = makeParsed();
216
+ const args = makeDefaultSegmentFetcherArgs();
217
+ const pipeline = makePipeline(parsed);
218
+ args.pipeline = pipeline;
219
+ const fetchSegment = createSegmentFetcher(args);
220
+ const onChunk = vi.fn();
221
+ const onAllChunksReceived = vi.fn();
222
+ const signal = new TaskCanceller("test run").signal;
223
+
224
+ await fetchSegment(
225
+ makeContent(),
226
+ { onChunk, onAllChunksReceived, onRetry: vi.fn() },
227
+ signal,
228
+ );
229
+
230
+ expect(mockScheduleRequestWithCdns).not.toHaveBeenCalled();
231
+ expect(onChunk).toHaveBeenCalledTimes(1);
232
+ // The parser function should invoke parseSegment
233
+ const parseFn = onChunk.mock.calls[0][0];
234
+ parseFn(undefined);
235
+ expect(pipeline.parseSegment).toHaveBeenCalledWith(
236
+ { data, isChunked: false },
237
+ expect.any(Object),
238
+ undefined,
239
+ );
240
+ });
241
+ });
242
+
243
+ describe("successful segment-loaded flow", () => {
244
+ it("calls onRequestBegin with a timestamp and id", async () => {
245
+ const args = makeDefaultSegmentFetcherArgs();
246
+ const fetchSegment = createSegmentFetcher(args);
247
+ const signal = new TaskCanceller("test run").signal;
248
+
249
+ await fetchSegment(
250
+ makeContent(),
251
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
252
+ signal,
253
+ );
254
+
255
+ expect(args.eventListeners.onRequestBegin).toHaveBeenCalledWith(
256
+ expect.objectContaining({ requestTimestamp: 1000 }),
257
+ );
258
+ });
259
+
260
+ it("calls onChunk once with a parse function", async () => {
261
+ const args = makeDefaultSegmentFetcherArgs();
262
+ const fetchSegment = createSegmentFetcher(args);
263
+ const onChunk = vi.fn();
264
+ const signal = new TaskCanceller("test run").signal;
265
+
266
+ await fetchSegment(
267
+ makeContent(),
268
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
269
+ signal,
270
+ );
271
+
272
+ expect(onChunk).toHaveBeenCalledTimes(1);
273
+ expect(typeof onChunk.mock.calls[0][0]).toBe("function");
274
+ });
275
+
276
+ it("calls onAllChunksReceived after onChunk", async () => {
277
+ const callOrder: string[] = [];
278
+ const args = makeDefaultSegmentFetcherArgs();
279
+ const fetchSegment = createSegmentFetcher(args);
280
+ const signal = new TaskCanceller("test run").signal;
281
+
282
+ await fetchSegment(
283
+ makeContent(),
284
+ {
285
+ onChunk: () => callOrder.push("chunk"),
286
+ onAllChunksReceived: () => callOrder.push("allChunks"),
287
+ onRetry: vi.fn(),
288
+ },
289
+ signal,
290
+ );
291
+
292
+ expect(callOrder).toEqual(["chunk", "allChunks"]);
293
+ });
294
+
295
+ it("adds loaded data to cache", async () => {
296
+ const args = makeDefaultSegmentFetcherArgs();
297
+ const fetchSegment = createSegmentFetcher(args);
298
+ const signal = new TaskCanceller("test run").signal;
299
+
300
+ await fetchSegment(
301
+ makeContent(),
302
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
303
+ signal,
304
+ );
305
+
306
+ expect(mockInitializationSegmentCacheAdd).toHaveBeenCalledWith(
307
+ expect.any(Object),
308
+ new Uint8Array([1, 2, 3]).buffer,
309
+ );
310
+ });
311
+
312
+ it("calls onRequestEnd after success", async () => {
313
+ const args = makeDefaultSegmentFetcherArgs();
314
+ const fetchSegment = createSegmentFetcher(args);
315
+ const signal = new TaskCanceller("test run").signal;
316
+
317
+ await fetchSegment(
318
+ makeContent(),
319
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
320
+ signal,
321
+ );
322
+
323
+ expect(args.eventListeners.onRequestEnd).toHaveBeenCalledWith(
324
+ expect.objectContaining({ id: expect.any(String) }),
325
+ );
326
+ });
327
+
328
+ it("sends metrics after chunk is parsed (media segment with duration)", async () => {
329
+ const parsed = makeParsed("media", 3);
330
+ const pipeline = makePipeline(parsed, makeLoadedResult());
331
+ const args = makeDefaultSegmentFetcherArgs();
332
+ args.pipeline = pipeline;
333
+ const fetchSegment = createSegmentFetcher(args);
334
+ const onChunk = vi.fn();
335
+ const signal = new TaskCanceller("test run").signal;
336
+
337
+ await fetchSegment(
338
+ makeContent(),
339
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
340
+ signal,
341
+ );
342
+
343
+ // parse the chunk to trigger metrics
344
+ onChunk.mock.calls[0][0](undefined);
345
+
346
+ expect(args.eventListeners.onMetrics).toHaveBeenCalledWith(
347
+ expect.objectContaining({
348
+ size: 3,
349
+ requestDuration: 1500,
350
+ segmentDuration: 3,
351
+ }),
352
+ );
353
+ });
354
+
355
+ it("does not send metrics twice for the same chunk", async () => {
356
+ const parsed = makeParsed("media", 3);
357
+ const pipeline = makePipeline(parsed, makeLoadedResult());
358
+ const args = makeDefaultSegmentFetcherArgs();
359
+ args.pipeline = pipeline;
360
+ const fetchSegment = createSegmentFetcher(args);
361
+ const onChunk = vi.fn();
362
+ const signal = new TaskCanceller("test run").signal;
363
+
364
+ await fetchSegment(
365
+ makeContent(),
366
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
367
+ signal,
368
+ );
369
+
370
+ const parseFn = onChunk.mock.calls[0][0];
371
+ parseFn(undefined);
372
+ parseFn(undefined);
373
+
374
+ expect(args.eventListeners.onMetrics).toHaveBeenCalledTimes(1);
375
+ });
376
+ });
377
+
378
+ describe("segment-created flow", () => {
379
+ it("calls onChunk and onAllChunksReceived for created segment", async () => {
380
+ const createdResult = makeCreatedResult();
381
+ const pipeline = makePipeline(makeParsed(), createdResult);
382
+ pipeline.loadSegment.mockResolvedValue(createdResult);
383
+ mockScheduleRequestWithCdns.mockImplementation(() => {
384
+ return new Promise<Promise<ISegmentLoaderResultSegmentCreated<ArrayBuffer>>>(
385
+ (res) => {
386
+ const response = Promise.resolve(createdResult);
387
+ res(response);
388
+ },
389
+ );
390
+ });
391
+
392
+ const args = makeDefaultSegmentFetcherArgs();
393
+ args.pipeline = pipeline;
394
+ const fetchSegment = createSegmentFetcher(args);
395
+ const onChunk = vi.fn();
396
+ const signal = new TaskCanceller("test run").signal;
397
+
398
+ await fetchSegment(
399
+ makeContent(),
400
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
401
+ signal,
402
+ );
403
+
404
+ expect(onChunk).toHaveBeenCalledTimes(1);
405
+ });
406
+
407
+ it("does not add created segment to cache", async () => {
408
+ const createdResult = makeCreatedResult();
409
+ mockScheduleRequestWithCdns.mockResolvedValue(createdResult);
410
+
411
+ const args = makeDefaultSegmentFetcherArgs();
412
+ const fetchSegment = createSegmentFetcher(args);
413
+ const signal = new TaskCanceller("test run").signal;
414
+
415
+ await fetchSegment(
416
+ makeContent(),
417
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
418
+ signal,
419
+ );
420
+
421
+ expect(mockInitializationSegmentCacheAdd).not.toHaveBeenCalled();
422
+ });
423
+
424
+ it("does not send metrics for created segments", async () => {
425
+ const createdResult = makeCreatedResult();
426
+ mockScheduleRequestWithCdns.mockResolvedValue(createdResult);
427
+
428
+ const args = makeDefaultSegmentFetcherArgs();
429
+ const fetchSegment = createSegmentFetcher(args);
430
+ const onChunk = vi.fn();
431
+ const signal = new TaskCanceller("test run").signal;
432
+
433
+ await fetchSegment(
434
+ makeContent(),
435
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
436
+ signal,
437
+ );
438
+
439
+ // parse the chunk
440
+ if (onChunk.mock.calls.length > 0) {
441
+ onChunk.mock.calls[0][0](undefined);
442
+ }
443
+
444
+ expect(args.eventListeners.onMetrics).not.toHaveBeenCalled();
445
+ });
446
+ });
447
+
448
+ describe("progress callback", () => {
449
+ it("calls onProgress when size < totalSize", async () => {
450
+ const args = makeDefaultSegmentFetcherArgs();
451
+ args.pipeline.loadSegment.mockImplementation(
452
+ async (
453
+ _cdn: unknown,
454
+ _ctx: unknown,
455
+ _opts: unknown,
456
+ _sig: unknown,
457
+ callbacks: ISegmentLoaderCallbacks<ArrayBuffer>,
458
+ ) => {
459
+ callbacks.onProgress({ duration: 500, size: 1, totalSize: 3 });
460
+ await sleep(0);
461
+ return makeLoadedResult();
462
+ },
463
+ );
464
+ const fetchSegment = createSegmentFetcher(args);
465
+ await fetchSegment(
466
+ makeContent(),
467
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
468
+ new TaskCanceller("test run").signal,
469
+ );
470
+ expect(args.eventListeners.onProgress).toHaveBeenCalledWith(
471
+ expect.objectContaining({ size: 1, totalSize: 3 }),
472
+ );
473
+ });
474
+
475
+ it("does not call onProgress when size >= totalSize", async () => {
476
+ const args = makeDefaultSegmentFetcherArgs();
477
+ args.pipeline.loadSegment.mockImplementation(
478
+ async (
479
+ _cdn: unknown,
480
+ _ctx: unknown,
481
+ _opts: unknown,
482
+ _sig: unknown,
483
+ callbacks: ISegmentLoaderCallbacks<ArrayBuffer>,
484
+ ) => {
485
+ callbacks.onProgress({ duration: 500, size: 3, totalSize: 3 });
486
+ return makeLoadedResult();
487
+ },
488
+ );
489
+ const fetchSegment = createSegmentFetcher(args);
490
+ await fetchSegment(
491
+ makeContent(),
492
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
493
+ new TaskCanceller("test run").signal,
494
+ );
495
+ expect(args.eventListeners.onProgress).not.toHaveBeenCalled();
496
+ });
497
+ });
498
+
499
+ describe("onNewChunk (chunked loading)", () => {
500
+ it("calls onChunk for each chunk received via onNewChunk", async () => {
501
+ const args = makeDefaultSegmentFetcherArgs();
502
+ args.pipeline.loadSegment.mockImplementation(
503
+ async (
504
+ _cdn: unknown,
505
+ _ctx: unknown,
506
+ _opts: unknown,
507
+ _sig: unknown,
508
+ callbacks: ISegmentLoaderCallbacks<ArrayBuffer>,
509
+ ) => {
510
+ callbacks.onNewChunk(new Uint8Array([1, 2, 3]).buffer);
511
+ callbacks.onNewChunk(new Uint8Array([4, 5, 6]).buffer);
512
+ return makeLoadedResult();
513
+ },
514
+ );
515
+ const fetchSegment = createSegmentFetcher(args);
516
+ const onChunk = vi.fn();
517
+ await fetchSegment(
518
+ makeContent(),
519
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
520
+ new TaskCanceller("test run").signal,
521
+ );
522
+ // 2 from onNewChunk + 1 from the final loaded data
523
+ expect(onChunk).toHaveBeenCalledTimes(3);
524
+ });
525
+ });
526
+
527
+ describe("error handling", () => {
528
+ it("throws and calls onRequestEnd on non-cancellation error", async () => {
529
+ const networkError = new Error("Network error");
530
+ mockScheduleRequestWithCdns.mockRejectedValue(networkError);
531
+ mockErrorSelector.mockReturnValue(networkError);
532
+
533
+ const args = makeDefaultSegmentFetcherArgs();
534
+ const fetchSegment = createSegmentFetcher(args);
535
+
536
+ await expect(
537
+ fetchSegment(
538
+ makeContent(),
539
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
540
+ new TaskCanceller("test run").signal,
541
+ ),
542
+ ).rejects.toThrow("Network error");
543
+
544
+ expect(args.eventListeners.onRequestEnd).toHaveBeenCalledWith(
545
+ expect.objectContaining({ id: expect.any(String) }),
546
+ );
547
+ });
548
+
549
+ it("re-throws CancellationError without calling onRequestEnd in catch", async () => {
550
+ const cancellationError = new CancellationError("test task", "test reason");
551
+ mockScheduleRequestWithCdns.mockRejectedValue(cancellationError);
552
+
553
+ const args = makeDefaultSegmentFetcherArgs();
554
+ const fetchSegment = createSegmentFetcher(args);
555
+ const signal = new TaskCanceller("test run").signal;
556
+
557
+ await expect(
558
+ fetchSegment(
559
+ makeContent(),
560
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
561
+ signal,
562
+ ),
563
+ ).rejects.toBeInstanceOf(CancellationError);
564
+
565
+ // onRequestEnd should NOT be called from the catch block for cancellation
566
+ expect(args.eventListeners.onRequestEnd).not.toHaveBeenCalled();
567
+ });
568
+
569
+ it("calls onRetry callback when a retry occurs", async () => {
570
+ const retryError = new Error("retry error");
571
+ mockErrorSelector.mockReturnValue(retryError);
572
+
573
+ // scheduleRequestWithCdns calls onRetry then succeeds
574
+ mockScheduleRequestWithCdns.mockImplementation(
575
+ async (
576
+ _cdns: unknown,
577
+ _prio: unknown,
578
+ // TODO: Why did I not succeed to define this?
579
+ performRequest: any,
580
+ opts: IBackoffSettings,
581
+ ) => {
582
+ opts.onRetry(retryError);
583
+ return performRequest(null, new TaskCanceller("test task").signal);
584
+ },
585
+ );
586
+
587
+ const onRetry = vi.fn();
588
+ const args = makeDefaultSegmentFetcherArgs();
589
+ const fetchSegment = createSegmentFetcher(args);
590
+ const signal = new TaskCanceller("test run").signal;
591
+
592
+ await fetchSegment(
593
+ makeContent(),
594
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry },
595
+ signal,
596
+ );
597
+
598
+ expect(onRetry).toHaveBeenCalledWith(retryError);
599
+ });
600
+
601
+ it("wraps parse errors with formatError", async () => {
602
+ const parseError = new Error("parse fail");
603
+ const wrappedError = new Error("PIPELINE_PARSE_ERROR");
604
+ mockFormatError.mockReturnValue(wrappedError);
605
+
606
+ const pipeline = makePipeline();
607
+ pipeline.parseSegment.mockImplementation(() => {
608
+ throw parseError;
609
+ });
610
+
611
+ const args = makeDefaultSegmentFetcherArgs();
612
+ args.pipeline = pipeline;
613
+ const fetchSegment = createSegmentFetcher(args);
614
+ const onChunk = vi.fn();
615
+ const signal = new TaskCanceller("test run").signal;
616
+
617
+ await fetchSegment(
618
+ makeContent(),
619
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
620
+ signal,
621
+ );
622
+
623
+ const parseFn = onChunk.mock.calls[0][0];
624
+ expect(() => parseFn(undefined)).toThrow(wrappedError);
625
+ expect(mockFormatError).toHaveBeenCalledWith(parseError, {
626
+ defaultCode: "PIPELINE_PARSE_ERROR",
627
+ defaultReason: "Unknown parsing error",
628
+ });
629
+ });
630
+ });
631
+
632
+ describe("cancellation", () => {
633
+ it("calls onRequestEnd from cancellation handler when request is in flight", async () => {
634
+ const args = makeDefaultSegmentFetcherArgs();
635
+ const fetchSegment = createSegmentFetcher(args);
636
+
637
+ const canceller = new TaskCanceller("test run");
638
+
639
+ mockScheduleRequestWithCdns.mockImplementation(async () => {
640
+ // Trigger cancellation callback before resolving
641
+ canceller.cancel("test reason");
642
+ throw canceller.signal.cancellationError;
643
+ });
644
+
645
+ await expect(
646
+ fetchSegment(
647
+ makeContent(),
648
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
649
+ canceller.signal,
650
+ ),
651
+ ).rejects.toBeInstanceOf(CancellationError);
652
+
653
+ // onRequestEnd should have been called by the onCancellation handler
654
+ expect(args.eventListeners.onRequestEnd).toHaveBeenCalledTimes(1);
655
+ });
656
+
657
+ it("does not call onRequestEnd from main flow if already cancelled", async () => {
658
+ const canceller = new TaskCanceller("test run");
659
+ canceller.cancel("test cancel");
660
+ mockScheduleRequestWithCdns.mockImplementation(async () => {
661
+ throw canceller.signal.cancellationError;
662
+ });
663
+
664
+ const args = makeDefaultSegmentFetcherArgs();
665
+ const fetchSegment = createSegmentFetcher(args);
666
+
667
+ await expect(
668
+ fetchSegment(
669
+ makeContent(),
670
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
671
+ canceller.signal,
672
+ ),
673
+ ).rejects.toBeInstanceOf(CancellationError);
674
+ });
675
+ });
676
+
677
+ describe("CMCD integration", () => {
678
+ it("passes cmcd payload to loadSegment when cmcdDataBuilder is set", async () => {
679
+ const cmcdPayload = { sessionId: "abc" };
680
+ const cmcdDataBuilder: CmcdDataBuilder = {
681
+ getCmcdDataForSegmentRequest: vi.fn().mockReturnValue(cmcdPayload),
682
+ } as any;
683
+
684
+ const args = makeDefaultSegmentFetcherArgs();
685
+ args.cmcdDataBuilder = cmcdDataBuilder;
686
+ const fetchSegment = createSegmentFetcher(args);
687
+ const signal = new TaskCanceller("test run").signal;
688
+
689
+ await fetchSegment(
690
+ makeContent(),
691
+ { onChunk: vi.fn(), onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
692
+ signal,
693
+ );
694
+
695
+ // eslint-disable-next-line @typescript-eslint/unbound-method
696
+ expect(cmcdDataBuilder.getCmcdDataForSegmentRequest).toHaveBeenCalled();
697
+ });
698
+ });
699
+
700
+ describe("requestTimeout and connectionTimeout configuration", () => {
701
+ it("sets timeout to undefined when requestTimeout is negative", () => {
702
+ const args = makeDefaultSegmentFetcherArgs();
703
+ args.requestOptions = {
704
+ baseDelay: 200,
705
+ maxDelay: 3000,
706
+ maxRetry: 3,
707
+ requestTimeout: -1,
708
+ connectionTimeout: 5000,
709
+ };
710
+ // just ensure it doesn't throw
711
+ expect(() => createSegmentFetcher(args)).not.toThrow();
712
+ });
713
+
714
+ it("sets connectionTimeout to undefined when it is negative", () => {
715
+ const args = makeDefaultSegmentFetcherArgs();
716
+ args.requestOptions = {
717
+ baseDelay: 200,
718
+ maxDelay: 3000,
719
+ maxRetry: 3,
720
+ requestTimeout: 30000,
721
+ connectionTimeout: -1,
722
+ };
723
+ expect(() => createSegmentFetcher(args)).not.toThrow();
724
+ });
725
+ });
726
+
727
+ describe("segmentDuration accumulation", () => {
728
+ it("accumulates duration across multiple chunks", async () => {
729
+ const args = makeDefaultSegmentFetcherArgs();
730
+ const fetchSegment = createSegmentFetcher(args);
731
+ const onChunk = vi.fn();
732
+ const signal = new TaskCanceller("test run").signal;
733
+
734
+ // two chunks via onNewChunk, then one final load result suppressed by returning null
735
+ args.pipeline.loadSegment.mockImplementation(
736
+ async (
737
+ _cdn: unknown,
738
+ _ctx: unknown,
739
+ _opts: unknown,
740
+ _sig: unknown,
741
+ callbacks: ISegmentLoaderCallbacks<ArrayBuffer>,
742
+ ) => {
743
+ callbacks.onNewChunk(new Uint8Array([0, 1, 2]).buffer);
744
+ callbacks.onNewChunk(new Uint8Array([3, 4, 5]).buffer);
745
+ // return a loaded segment but with 0 size to simplify metrics trigger
746
+ return {
747
+ resultType: "segment-loaded",
748
+ resultData: {
749
+ responseData: new Uint8Array([4, 6, 7]).buffer,
750
+ size: 3,
751
+ requestDuration: 1000,
752
+ },
753
+ };
754
+ },
755
+ );
756
+
757
+ // chunk-a → duration 2, chunk-b → duration 3, final → duration 1
758
+ let callCount = 0;
759
+ args.pipeline.parseSegment.mockImplementation(() => {
760
+ callCount++;
761
+ return {
762
+ segmentType: "media",
763
+ chunkData: new Uint8Array([4, 5, 6]).buffer,
764
+ chunkOffset: 0,
765
+ appendWindow: [0, Number.MAX_SAFE_INTEGER],
766
+ chunkSize: 3,
767
+ protectionData: [],
768
+ chunkInfos: { duration: callCount, time: 0 },
769
+ };
770
+ });
771
+
772
+ await fetchSegment(
773
+ makeContent(),
774
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
775
+ signal,
776
+ );
777
+
778
+ // parse all chunks
779
+ onChunk.mock.calls.forEach(([parseFn]: Array<(x?: unknown) => void>) =>
780
+ parseFn(undefined),
781
+ );
782
+
783
+ expect(args.eventListeners.onMetrics).toHaveBeenCalledWith(
784
+ expect.objectContaining({ segmentDuration: 1 + 2 + 3 }),
785
+ );
786
+ });
787
+
788
+ it("sets segmentDuration to undefined if a chunk has no duration info", async () => {
789
+ const args = makeDefaultSegmentFetcherArgs();
790
+ const fetchSegment = createSegmentFetcher(args);
791
+ const onChunk = vi.fn();
792
+ const signal = new TaskCanceller("test run").signal;
793
+
794
+ args.pipeline.parseSegment.mockReturnValue({
795
+ segmentType: "media",
796
+ chunkData: new Uint8Array([4, 5, 6]).buffer,
797
+ chunkOffset: 0,
798
+ appendWindow: [0, Number.MAX_SAFE_INTEGER],
799
+ chunkSize: 3,
800
+ protectionData: [],
801
+ chunkInfos: { duration: undefined, time: 0 }, // no duration
802
+ });
803
+
804
+ await fetchSegment(
805
+ makeContent(),
806
+ { onChunk, onAllChunksReceived: vi.fn(), onRetry: vi.fn() },
807
+ signal,
808
+ );
809
+
810
+ onChunk.mock.calls.forEach(([parseFn]: Array<(x?: unknown) => void>) =>
811
+ parseFn(undefined),
812
+ );
813
+
814
+ expect(args.eventListeners.onMetrics).toHaveBeenCalledWith(
815
+ expect.objectContaining({ segmentDuration: undefined }),
816
+ );
817
+ });
818
+ });
819
+ });
820
+
821
+ describe("getSegmentFetcherRequestOptions", () => {
822
+ beforeEach(() => {
823
+ mockGetCurrent.mockReturnValue({
824
+ DEFAULT_MAX_REQUESTS_RETRY_ON_ERROR: 4,
825
+ DEFAULT_REQUEST_TIMEOUT: 10000,
826
+ DEFAULT_CONNECTION_TIMEOUT: 2000,
827
+ INITIAL_BACKOFF_DELAY_BASE: { LOW_LATENCY: 100, REGULAR: 200 },
828
+ MAX_BACKOFF_DELAY_BASE: { LOW_LATENCY: 1000, REGULAR: 3000 },
829
+ });
830
+ });
831
+
832
+ it("uses defaults from config when options not provided", () => {
833
+ const result = getSegmentFetcherRequestOptions({ lowLatencyMode: false });
834
+ expect(result).toEqual({
835
+ maxRetry: 4,
836
+ baseDelay: 200,
837
+ maxDelay: 3000,
838
+ requestTimeout: 10000,
839
+ connectionTimeout: 2000,
840
+ });
841
+ });
842
+
843
+ it("uses low-latency base/max delays when lowLatencyMode is true", () => {
844
+ const result = getSegmentFetcherRequestOptions({ lowLatencyMode: true });
845
+ expect(result.baseDelay).toBe(100);
846
+ expect(result.maxDelay).toBe(1000);
847
+ });
848
+
849
+ it("uses provided maxRetry over default", () => {
850
+ const result = getSegmentFetcherRequestOptions({
851
+ lowLatencyMode: false,
852
+ maxRetry: 10,
853
+ });
854
+ expect(result.maxRetry).toBe(10);
855
+ });
856
+
857
+ it("uses provided requestTimeout over default", () => {
858
+ const result = getSegmentFetcherRequestOptions({
859
+ lowLatencyMode: false,
860
+ requestTimeout: 5000,
861
+ });
862
+ expect(result.requestTimeout).toBe(5000);
863
+ });
864
+
865
+ it("uses provided connectionTimeout over default", () => {
866
+ const result = getSegmentFetcherRequestOptions({
867
+ lowLatencyMode: false,
868
+ connectionTimeout: 999,
869
+ });
870
+ expect(result.connectionTimeout).toBe(999);
871
+ });
872
+ });