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.
- package/CHANGELOG.md +21 -3
- package/VERSION +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
- package/dist/commonjs/compat/should_seek_at_zero.d.ts +12 -0
- package/dist/commonjs/compat/should_seek_at_zero.d.ts.map +1 -0
- package/dist/commonjs/compat/should_seek_at_zero.js +23 -0
- package/dist/commonjs/core/cmcd/cmcd_data_builder.js +2 -2
- package/dist/commonjs/core/entry/FreezeResolver.js +2 -2
- package/dist/commonjs/core/entry/content_preparer.d.ts +1 -6
- package/dist/commonjs/core/entry/content_preparer.d.ts.map +1 -1
- package/dist/commonjs/core/entry/content_preparer.js +2 -125
- package/dist/commonjs/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
- package/dist/commonjs/core/entry/content_time_boundaries_observer.js +8 -0
- package/dist/commonjs/core/entry/core_entry.d.ts +1 -1
- package/dist/commonjs/core/entry/core_entry.d.ts.map +1 -1
- package/dist/commonjs/core/entry/core_entry.js +2 -7
- package/dist/commonjs/core/entry/core_text_displayer_interface.d.ts.map +1 -1
- package/dist/commonjs/core/entry/core_text_displayer_interface.js +3 -1
- package/dist/commonjs/core/entry/index.d.ts +1 -1
- package/dist/commonjs/core/entry/index.d.ts.map +1 -1
- package/dist/commonjs/core/entry/utils.d.ts +64 -0
- package/dist/commonjs/core/entry/utils.d.ts.map +1 -1
- package/dist/commonjs/core/entry/utils.js +165 -0
- package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts +5 -6
- package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts.map +1 -1
- package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
- package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.js +34 -30
- package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts +70 -0
- package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
- package/dist/commonjs/core/stream/representation/encryption_data_notifier.js +107 -0
- package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
- package/dist/commonjs/core/stream/representation/representation_stream.js +12 -60
- package/dist/commonjs/core/stream/representation/utils/get_needed_segments.js +1 -1
- package/dist/commonjs/default_config.d.ts +5 -0
- package/dist/commonjs/default_config.d.ts.map +1 -1
- package/dist/commonjs/default_config.js +5 -0
- package/dist/commonjs/experimental/features/worker/index.d.ts +2 -1
- package/dist/commonjs/experimental/features/worker/index.d.ts.map +1 -1
- package/dist/commonjs/experimental/features/worker/index.js +4 -3
- package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
- package/dist/commonjs/main_thread/api/public_api.js +28 -4
- package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
- package/dist/commonjs/main_thread/init/directfile_content_initializer.js +23 -1
- package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
- package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +3 -1
- package/dist/commonjs/manifest/classes/index.d.ts +2 -1
- package/dist/commonjs/manifest/classes/index.d.ts.map +1 -1
- package/dist/commonjs/manifest/classes/representation.d.ts +1 -1
- package/dist/commonjs/manifest/classes/representation.d.ts.map +1 -1
- package/dist/commonjs/manifest/classes/representation.js +2 -2
- package/dist/commonjs/manifest/classes/representation_index/types.d.ts +2 -2
- package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
- package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
- package/dist/commonjs/utils/base64.d.ts +15 -1
- package/dist/commonjs/utils/base64.d.ts.map +1 -1
- package/dist/commonjs/utils/base64.js +6 -0
- package/dist/commonjs/utils/ranges.d.ts +1 -1
- package/dist/commonjs/utils/ranges.js +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
- package/dist/es2017/compat/should_seek_at_zero.d.ts +12 -0
- package/dist/es2017/compat/should_seek_at_zero.d.ts.map +1 -0
- package/dist/es2017/compat/should_seek_at_zero.js +20 -0
- package/dist/es2017/core/cmcd/cmcd_data_builder.js +2 -2
- package/dist/es2017/core/entry/FreezeResolver.js +1 -1
- package/dist/es2017/core/entry/content_preparer.d.ts +1 -6
- package/dist/es2017/core/entry/content_preparer.d.ts.map +1 -1
- package/dist/es2017/core/entry/content_preparer.js +1 -76
- package/dist/es2017/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
- package/dist/es2017/core/entry/content_time_boundaries_observer.js +8 -0
- package/dist/es2017/core/entry/core_entry.d.ts +1 -1
- package/dist/es2017/core/entry/core_entry.d.ts.map +1 -1
- package/dist/es2017/core/entry/core_entry.js +2 -7
- package/dist/es2017/core/entry/core_text_displayer_interface.d.ts.map +1 -1
- package/dist/es2017/core/entry/core_text_displayer_interface.js +3 -1
- package/dist/es2017/core/entry/index.d.ts +1 -1
- package/dist/es2017/core/entry/index.d.ts.map +1 -1
- package/dist/es2017/core/entry/utils.d.ts +64 -0
- package/dist/es2017/core/entry/utils.d.ts.map +1 -1
- package/dist/es2017/core/entry/utils.js +98 -0
- package/dist/es2017/core/fetchers/segment/segment_queue.d.ts +5 -6
- package/dist/es2017/core/fetchers/segment/segment_queue.d.ts.map +1 -1
- package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
- package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.js +21 -16
- package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts +70 -0
- package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
- package/dist/es2017/core/stream/representation/encryption_data_notifier.js +80 -0
- package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
- package/dist/es2017/core/stream/representation/representation_stream.js +10 -38
- package/dist/es2017/core/stream/representation/utils/get_needed_segments.js +1 -1
- package/dist/es2017/default_config.d.ts +5 -0
- package/dist/es2017/default_config.d.ts.map +1 -1
- package/dist/es2017/default_config.js +5 -0
- package/dist/es2017/experimental/features/worker/index.d.ts +2 -1
- package/dist/es2017/experimental/features/worker/index.d.ts.map +1 -1
- package/dist/es2017/experimental/features/worker/index.js +2 -3
- package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
- package/dist/es2017/main_thread/api/public_api.js +28 -4
- package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
- package/dist/es2017/main_thread/init/directfile_content_initializer.js +23 -1
- package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
- package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +3 -1
- package/dist/es2017/manifest/classes/index.d.ts +2 -1
- package/dist/es2017/manifest/classes/index.d.ts.map +1 -1
- package/dist/es2017/manifest/classes/representation.d.ts +1 -1
- package/dist/es2017/manifest/classes/representation.d.ts.map +1 -1
- package/dist/es2017/manifest/classes/representation.js +1 -1
- package/dist/es2017/manifest/classes/representation_index/types.d.ts +2 -2
- package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
- package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
- package/dist/es2017/utils/base64.d.ts +15 -1
- package/dist/es2017/utils/base64.d.ts.map +1 -1
- package/dist/es2017/utils/base64.js +6 -0
- package/dist/es2017/utils/ranges.d.ts +1 -1
- package/dist/es2017/utils/ranges.js +1 -1
- package/dist/mpd-parser.wasm +0 -0
- package/dist/worker.js +8 -8
- package/package.json +26 -23
- package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
- package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
- package/src/compat/should_seek_at_zero.ts +23 -0
- package/src/core/adaptive/__tests__/adaptive_representation_selector.test.ts +734 -0
- package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +793 -0
- package/src/core/adaptive/__tests__/network_analyzer.test.ts +514 -0
- package/src/core/adaptive/utils/__tests__/bandwidth_estimator.test.ts +117 -0
- package/src/core/adaptive/utils/__tests__/last_estimate_storage.test.ts +76 -0
- package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +288 -0
- package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +249 -0
- package/src/core/cmcd/__tests__/cmcd_data_builder.test.ts +464 -0
- package/src/core/cmcd/cmcd_data_builder.ts +2 -2
- package/src/core/entry/FreezeResolver.ts +1 -1
- package/src/core/entry/__tests__/FreezeResolver.test.ts +542 -0
- package/src/core/entry/__tests__/content_time_boundaries_observer.test.ts +867 -0
- package/src/core/entry/__tests__/core_text_displayer_interface.test.ts +205 -0
- package/src/core/entry/__tests__/get_buffered_data_per_media_buffer.test.ts +95 -0
- package/src/core/entry/__tests__/track_choice_setter.test.ts +288 -0
- package/src/core/entry/__tests__/utils.test.ts +309 -0
- package/src/core/entry/content_preparer.ts +7 -118
- package/src/core/entry/content_time_boundaries_observer.ts +10 -0
- package/src/core/entry/core_entry.ts +7 -14
- package/src/core/entry/core_text_displayer_interface.ts +3 -1
- package/src/core/entry/index.ts +1 -1
- package/src/core/entry/utils.ts +151 -0
- package/src/core/fetchers/__tests__/cdn_prioritizer.test.ts +166 -0
- package/src/core/fetchers/manifest/__tests__/manifest_fetcher.test.ts +803 -0
- package/src/core/fetchers/segment/__tests__/segment_fetcher.test.ts +872 -0
- package/src/core/fetchers/segment/__tests__/segment_queue.test.ts +557 -0
- package/src/core/fetchers/segment/segment_queue.ts +5 -6
- package/src/core/fetchers/thumbnails/__tests__/thumbnail_fetcher.test.ts +435 -0
- package/src/core/fetchers/thumbnails/thumbnail_fetcher.ts +28 -20
- package/src/core/fetchers/utils/__tests__/schedule_request.test.ts +331 -0
- package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +593 -0
- package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +646 -0
- package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +531 -0
- package/src/core/stream/representation/encryption_data_notifier.ts +122 -0
- package/src/core/stream/representation/representation_stream.ts +11 -49
- package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +321 -0
- package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +513 -0
- package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +132 -0
- package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +209 -0
- package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +343 -0
- package/src/core/stream/representation/utils/get_needed_segments.ts +1 -1
- package/src/default_config.ts +6 -0
- package/src/experimental/features/worker/index.ts +2 -4
- package/src/main_thread/api/public_api.ts +33 -4
- package/src/main_thread/init/directfile_content_initializer.ts +26 -1
- package/src/main_thread/init/utils/initial_seek_and_play.ts +5 -1
- package/src/manifest/classes/index.ts +2 -0
- package/src/manifest/classes/representation.ts +2 -1
- package/src/manifest/classes/representation_index/types.ts +2 -2
- package/src/mse/__tests__/main_media_source_interface.test.ts +846 -0
- package/src/parsers/texttracks/ttml/html/apply_line_height.ts +16 -1
- package/src/utils/__tests__/base64.test.ts +159 -191
- package/src/utils/base64.ts +30 -1
- package/src/utils/ranges.ts +1 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import type { Mock } from "vitest";
|
|
2
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
3
|
+
import type { ICdnMetadata } from "../../../../parsers/manifest";
|
|
4
|
+
import type {
|
|
5
|
+
IRequestedData,
|
|
6
|
+
IThumbnailLoader,
|
|
7
|
+
IThumbnailParser,
|
|
8
|
+
} from "../../../../transports";
|
|
9
|
+
import type { CancellationSignal } from "../../../../utils/task_canceller";
|
|
10
|
+
import TaskCanceller, { CancellationError } from "../../../../utils/task_canceller";
|
|
11
|
+
import type CdnPrioritizer from "../../cdn_prioritizer";
|
|
12
|
+
import type { IBackoffSettings } from "../../utils/schedule_request";
|
|
13
|
+
import createThumbnailFetcher from "../thumbnail_fetcher";
|
|
14
|
+
|
|
15
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
16
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
17
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
mockConfig,
|
|
21
|
+
mockLog,
|
|
22
|
+
mockScheduleRequestWithCdns,
|
|
23
|
+
mockFormatError,
|
|
24
|
+
mockErrorSelector,
|
|
25
|
+
} = vi.hoisted(() => {
|
|
26
|
+
return {
|
|
27
|
+
mockConfig: {
|
|
28
|
+
getCurrent: vi.fn(() => ({
|
|
29
|
+
DEFAULT_MAX_THUMBNAIL_REQUESTS_RETRY_ON_ERROR: 3,
|
|
30
|
+
DEFAULT_THUMBNAIL_REQUEST_TIMEOUT: 10000,
|
|
31
|
+
DEFAULT_THUMBNAIL_CONNECTION_TIMEOUT: 5000,
|
|
32
|
+
INITIAL_BACKOFF_DELAY_BASE: { REGULAR: 200 },
|
|
33
|
+
MAX_BACKOFF_DELAY_BASE: { REGULAR: 3000 },
|
|
34
|
+
})),
|
|
35
|
+
},
|
|
36
|
+
mockLog: {
|
|
37
|
+
debug: vi.fn(),
|
|
38
|
+
warn: vi.fn(),
|
|
39
|
+
error: vi.fn(),
|
|
40
|
+
},
|
|
41
|
+
mockScheduleRequestWithCdns: vi.fn(
|
|
42
|
+
(
|
|
43
|
+
cdns: ICdnMetadata[] | null,
|
|
44
|
+
_cdnPrioritizer: CdnPrioritizer | null,
|
|
45
|
+
performRequest: (
|
|
46
|
+
cdn: ICdnMetadata | null,
|
|
47
|
+
cancellationSignal: CancellationSignal,
|
|
48
|
+
) => Promise<IRequestedData<ArrayBuffer>>,
|
|
49
|
+
_options: IBackoffSettings,
|
|
50
|
+
globalCancellationSignal: CancellationSignal,
|
|
51
|
+
): Promise<IRequestedData<ArrayBuffer>> => {
|
|
52
|
+
if (globalCancellationSignal.isCancelled()) {
|
|
53
|
+
return Promise.reject(globalCancellationSignal.cancellationError);
|
|
54
|
+
}
|
|
55
|
+
return performRequest(cdns?.[0] ?? null, new TaskCanceller("test task").signal);
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
mockFormatError: vi.fn((err: unknown): any => err),
|
|
59
|
+
mockErrorSelector: vi.fn((err: unknown): any => err),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
vi.mock("../../../../config", () => ({ default: mockConfig }));
|
|
64
|
+
vi.mock("../../../../log", () => ({ default: mockLog }));
|
|
65
|
+
vi.mock("../../utils/schedule_request", () => ({
|
|
66
|
+
scheduleRequestWithCdns: mockScheduleRequestWithCdns,
|
|
67
|
+
}));
|
|
68
|
+
vi.mock("../../../../errors", () => ({ formatError: mockFormatError }));
|
|
69
|
+
vi.mock("../../utils/error_selector", () => ({ default: mockErrorSelector }));
|
|
70
|
+
|
|
71
|
+
describe("createThumbnailFetcher", () => {
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
vi.resetAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns a function (fetchThumbnail)", () => {
|
|
77
|
+
const fetcher = createThumbnailFetcher(makePipeline(), null);
|
|
78
|
+
expect(typeof fetcher).toBe("function");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("successful fetch", () => {
|
|
82
|
+
it("calls loadThumbnail and parseThumbnail, returns parsed result", async () => {
|
|
83
|
+
const pipeline = makePipeline(
|
|
84
|
+
() =>
|
|
85
|
+
Promise.resolve({
|
|
86
|
+
responseData: new Uint8Array([0, 1, 2]).buffer,
|
|
87
|
+
size: 3,
|
|
88
|
+
requestDuration: 1000,
|
|
89
|
+
}),
|
|
90
|
+
(d) => ({ data: d, mimeType: "image/png", thumbnails: [] }),
|
|
91
|
+
);
|
|
92
|
+
pipeline.loadThumbnail.mockResolvedValue({
|
|
93
|
+
responseData: new Uint8Array([7, 7, 7]).buffer,
|
|
94
|
+
size: 3,
|
|
95
|
+
requestDuration: 500,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
99
|
+
const canceller = new TaskCanceller("test");
|
|
100
|
+
const ctx = makeThumbnailContext();
|
|
101
|
+
|
|
102
|
+
const result = await fetcher(ctx, canceller.signal);
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
data: new Uint8Array([7, 7, 7]).buffer,
|
|
105
|
+
mimeType: "image/png",
|
|
106
|
+
thumbnails: [],
|
|
107
|
+
});
|
|
108
|
+
expect(pipeline.parseThumbnail).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("request deduplication", () => {
|
|
113
|
+
it("shares the same promise when the same thumbnail is requested twice concurrently", async () => {
|
|
114
|
+
const pipeline = makePipeline();
|
|
115
|
+
let resolveRequest!: (v: IRequestedData<ArrayBuffer>) => void;
|
|
116
|
+
mockScheduleRequestWithCdns.mockReturnValue(
|
|
117
|
+
new Promise((res) => {
|
|
118
|
+
resolveRequest = res;
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
pipeline.parseThumbnail.mockReturnValue({
|
|
122
|
+
mimeType: "image/gif",
|
|
123
|
+
data: new Uint8Array([4, 2, 2]).buffer,
|
|
124
|
+
thumbnails: [],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
128
|
+
const canceller1 = new TaskCanceller("test1");
|
|
129
|
+
const canceller2 = new TaskCanceller("test2");
|
|
130
|
+
const ctx = makeThumbnailContext();
|
|
131
|
+
|
|
132
|
+
const p1 = fetcher(ctx, canceller1.signal);
|
|
133
|
+
const p2 = fetcher(ctx, canceller2.signal);
|
|
134
|
+
|
|
135
|
+
// Only one network call should have been made
|
|
136
|
+
expect(mockScheduleRequestWithCdns).toHaveBeenCalledTimes(1);
|
|
137
|
+
|
|
138
|
+
resolveRequest({
|
|
139
|
+
responseData: new Uint8Array([5, 6, 7]).buffer,
|
|
140
|
+
size: 3,
|
|
141
|
+
requestDuration: 900,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
145
|
+
expect(r1).toEqual({
|
|
146
|
+
mimeType: "image/gif",
|
|
147
|
+
data: new Uint8Array([4, 2, 2]).buffer,
|
|
148
|
+
thumbnails: [],
|
|
149
|
+
});
|
|
150
|
+
expect(r2).toEqual({
|
|
151
|
+
mimeType: "image/gif",
|
|
152
|
+
data: new Uint8Array([4, 2, 2]).buffer,
|
|
153
|
+
thumbnails: [],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("makes a new request when thumbnails differ", async () => {
|
|
158
|
+
const pipeline = makePipeline();
|
|
159
|
+
mockScheduleRequestWithCdns.mockResolvedValue({
|
|
160
|
+
responseData: new Uint8Array([7, 8, 7]).buffer,
|
|
161
|
+
size: 3,
|
|
162
|
+
requestDuration: 500,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
166
|
+
const canceller1 = new TaskCanceller("test1");
|
|
167
|
+
const canceller2 = new TaskCanceller("test2");
|
|
168
|
+
|
|
169
|
+
// Fetch sequentially so there is no overlap
|
|
170
|
+
await fetcher(makeThumbnailContext({ segmentId: "seg-1" }), canceller1.signal);
|
|
171
|
+
await fetcher(makeThumbnailContext({ segmentId: "seg-2" }), canceller2.signal);
|
|
172
|
+
|
|
173
|
+
expect(mockScheduleRequestWithCdns).toHaveBeenCalledTimes(2);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("cancellation", () => {
|
|
178
|
+
it("rejects with CancellationError when the signal is already cancelled on entry", async () => {
|
|
179
|
+
const pipeline = makePipeline();
|
|
180
|
+
let resolveRequest!: (v: IRequestedData<ArrayBuffer>) => void;
|
|
181
|
+
mockScheduleRequestWithCdns.mockReturnValue(
|
|
182
|
+
new Promise((res) => {
|
|
183
|
+
resolveRequest = res;
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
188
|
+
const canceller = new TaskCanceller("test");
|
|
189
|
+
const ctx = makeThumbnailContext();
|
|
190
|
+
|
|
191
|
+
const fetchPromise = fetcher(ctx, canceller.signal);
|
|
192
|
+
canceller.cancel("cancelled");
|
|
193
|
+
resolveRequest({
|
|
194
|
+
responseData: new Uint8Array([9, 7, 7]).buffer,
|
|
195
|
+
size: 3,
|
|
196
|
+
requestDuration: 500,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await expect(fetchPromise).rejects.toBeInstanceOf(CancellationError);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("cancels the underlying request when all callers cancel", async () => {
|
|
203
|
+
const pipeline = makePipeline();
|
|
204
|
+
let rejectRequest!: (e: unknown) => void;
|
|
205
|
+
mockScheduleRequestWithCdns.mockReturnValue(
|
|
206
|
+
new Promise((_res, rej) => {
|
|
207
|
+
rejectRequest = rej;
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
212
|
+
const canceller1 = new TaskCanceller("test1");
|
|
213
|
+
const canceller2 = new TaskCanceller("test2");
|
|
214
|
+
const ctx = makeThumbnailContext();
|
|
215
|
+
|
|
216
|
+
const p1 = fetcher(ctx, canceller1.signal);
|
|
217
|
+
const p2 = fetcher(ctx, canceller2.signal);
|
|
218
|
+
|
|
219
|
+
// Only one network request made
|
|
220
|
+
expect(mockScheduleRequestWithCdns).toHaveBeenCalledTimes(1);
|
|
221
|
+
|
|
222
|
+
canceller1.cancel("cancel1");
|
|
223
|
+
// Request should still be live because p2 is still pending
|
|
224
|
+
canceller2.cancel("cancel2");
|
|
225
|
+
|
|
226
|
+
// Trigger the underlying rejection to settle the promises
|
|
227
|
+
const cancelErr = new CancellationError("test task", "aborted");
|
|
228
|
+
rejectRequest(cancelErr);
|
|
229
|
+
|
|
230
|
+
await expect(p1).rejects.toBeInstanceOf(CancellationError);
|
|
231
|
+
await expect(p2).rejects.toBeInstanceOf(CancellationError);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("keeps the request alive when only one of two callers cancels", async () => {
|
|
235
|
+
const pipeline = makePipeline();
|
|
236
|
+
let resolveRequest!: (v: IRequestedData<ArrayBuffer>) => void;
|
|
237
|
+
mockScheduleRequestWithCdns.mockReturnValue(
|
|
238
|
+
new Promise((res) => {
|
|
239
|
+
resolveRequest = res;
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
244
|
+
const canceller1 = new TaskCanceller("test1");
|
|
245
|
+
const canceller2 = new TaskCanceller("test2");
|
|
246
|
+
const ctx = makeThumbnailContext();
|
|
247
|
+
|
|
248
|
+
const p1 = fetcher(ctx, canceller1.signal);
|
|
249
|
+
const p2 = fetcher(ctx, canceller2.signal);
|
|
250
|
+
|
|
251
|
+
canceller1.cancel("only first cancels");
|
|
252
|
+
resolveRequest({
|
|
253
|
+
responseData: new Uint8Array([4, 7, 7]).buffer,
|
|
254
|
+
size: 3,
|
|
255
|
+
requestDuration: 500,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// p2 should still resolve
|
|
259
|
+
const result = await p2;
|
|
260
|
+
expect(result).toEqual({
|
|
261
|
+
data: new Uint8Array([4, 7, 7]).buffer,
|
|
262
|
+
|
|
263
|
+
// Default mocked characteristics
|
|
264
|
+
mimeType: "image/png",
|
|
265
|
+
thumbnails: [
|
|
266
|
+
{
|
|
267
|
+
height: 720,
|
|
268
|
+
width: 1280,
|
|
269
|
+
offsetX: 0,
|
|
270
|
+
offsetY: 0,
|
|
271
|
+
start: 0,
|
|
272
|
+
end: Number.MAX_SAFE_INTEGER,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// p1 should reject
|
|
278
|
+
await expect(p1).rejects.toBeInstanceOf(CancellationError);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("error handling", () => {
|
|
283
|
+
it("throws a formatted error when the network request fails with a non-cancellation error", async () => {
|
|
284
|
+
const networkError = new Error("network failure");
|
|
285
|
+
const formattedError = new Error("formatted request error");
|
|
286
|
+
mockScheduleRequestWithCdns.mockRejectedValue(networkError);
|
|
287
|
+
mockErrorSelector.mockReturnValue(formattedError);
|
|
288
|
+
|
|
289
|
+
const pipeline = makePipeline();
|
|
290
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
291
|
+
const canceller = new TaskCanceller("test");
|
|
292
|
+
|
|
293
|
+
await expect(fetcher(makeThumbnailContext(), canceller.signal)).rejects.toBe(
|
|
294
|
+
formattedError,
|
|
295
|
+
);
|
|
296
|
+
expect(mockErrorSelector).toHaveBeenCalledWith(networkError);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("throws a formatted error when parseThumbnail throws", async () => {
|
|
300
|
+
const parseError = new Error("parse boom");
|
|
301
|
+
const formattedParseError = new Error("formatted parse error");
|
|
302
|
+
const pipeline = makePipeline();
|
|
303
|
+
pipeline.loadThumbnail.mockResolvedValue({
|
|
304
|
+
responseData: new Uint8Array([0, 1, 2]).buffer,
|
|
305
|
+
size: 3,
|
|
306
|
+
requestDuration: 1100,
|
|
307
|
+
});
|
|
308
|
+
pipeline.parseThumbnail.mockImplementation(() => {
|
|
309
|
+
throw parseError;
|
|
310
|
+
});
|
|
311
|
+
mockFormatError.mockReturnValue(formattedParseError);
|
|
312
|
+
|
|
313
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
314
|
+
const canceller = new TaskCanceller("test");
|
|
315
|
+
|
|
316
|
+
await expect(fetcher(makeThumbnailContext(), canceller.signal)).rejects.toBe(
|
|
317
|
+
formattedParseError,
|
|
318
|
+
);
|
|
319
|
+
expect(mockFormatError).toHaveBeenCalledWith(
|
|
320
|
+
parseError,
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
defaultCode: "PIPELINE_PARSE_ERROR",
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("propagates CancellationError from scheduleRequestWithCdns without wrapping", async () => {
|
|
328
|
+
const cancelErr = new CancellationError("test task", "aborted");
|
|
329
|
+
mockScheduleRequestWithCdns.mockRejectedValue(cancelErr);
|
|
330
|
+
|
|
331
|
+
const pipeline = makePipeline();
|
|
332
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
333
|
+
const canceller = new TaskCanceller("test");
|
|
334
|
+
|
|
335
|
+
await expect(fetcher(makeThumbnailContext(), canceller.signal)).rejects.toBe(
|
|
336
|
+
cancelErr,
|
|
337
|
+
);
|
|
338
|
+
// errorSelector should not be called for CancellationError
|
|
339
|
+
expect(mockErrorSelector).not.toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("retry logging", () => {
|
|
344
|
+
it("calls onRetry and logs a warning when the request retries", async () => {
|
|
345
|
+
const retryError = new Error("retry me");
|
|
346
|
+
const formattedRetryError = new Error("formatted retry");
|
|
347
|
+
mockErrorSelector.mockReturnValue(formattedRetryError);
|
|
348
|
+
|
|
349
|
+
mockScheduleRequestWithCdns.mockImplementation(
|
|
350
|
+
async (
|
|
351
|
+
_cdns: ICdnMetadata[] | null,
|
|
352
|
+
_cdnPrioritizer: CdnPrioritizer | null,
|
|
353
|
+
_performRequest: (
|
|
354
|
+
cdn: ICdnMetadata | null,
|
|
355
|
+
cancellationSignal: CancellationSignal,
|
|
356
|
+
) => Promise<IRequestedData<ArrayBuffer>>,
|
|
357
|
+
options: IBackoffSettings,
|
|
358
|
+
_globalCancellationSignal: CancellationSignal,
|
|
359
|
+
) => {
|
|
360
|
+
// Simulate the retry callback being invoked before eventual failure
|
|
361
|
+
options.onRetry?.(retryError);
|
|
362
|
+
throw retryError;
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
mockErrorSelector.mockReturnValue(formattedRetryError);
|
|
366
|
+
|
|
367
|
+
const pipeline = makePipeline();
|
|
368
|
+
const fetcher = createThumbnailFetcher(pipeline, null);
|
|
369
|
+
const canceller = new TaskCanceller("test");
|
|
370
|
+
|
|
371
|
+
await expect(
|
|
372
|
+
fetcher(makeThumbnailContext(), canceller.signal),
|
|
373
|
+
).rejects.toBeDefined();
|
|
374
|
+
expect(mockLog.warn).toHaveBeenCalledWith(
|
|
375
|
+
"Thumbnails",
|
|
376
|
+
"Thumbnail request retry ",
|
|
377
|
+
expect.any(Object),
|
|
378
|
+
formattedRetryError,
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Construct `pipeline` argument for a ThumbnailFetcher
|
|
386
|
+
*/
|
|
387
|
+
function makePipeline(
|
|
388
|
+
loadThumbnailImpl?: IThumbnailLoader,
|
|
389
|
+
parseThumbnailImpl?: IThumbnailParser,
|
|
390
|
+
): {
|
|
391
|
+
loadThumbnail: Mock<IThumbnailLoader>;
|
|
392
|
+
parseThumbnail: Mock<IThumbnailParser>;
|
|
393
|
+
} {
|
|
394
|
+
return {
|
|
395
|
+
loadThumbnail: vi.fn(
|
|
396
|
+
loadThumbnailImpl ??
|
|
397
|
+
(() =>
|
|
398
|
+
Promise.resolve({
|
|
399
|
+
responseData: new Uint8Array([0, 1, 2]).buffer,
|
|
400
|
+
requestDuration: undefined,
|
|
401
|
+
url: undefined,
|
|
402
|
+
sendingTime: undefined,
|
|
403
|
+
receivedTime: undefined,
|
|
404
|
+
size: undefined,
|
|
405
|
+
})),
|
|
406
|
+
),
|
|
407
|
+
parseThumbnail: vi.fn(
|
|
408
|
+
parseThumbnailImpl ??
|
|
409
|
+
((data: ArrayBuffer) => ({
|
|
410
|
+
data,
|
|
411
|
+
mimeType: "image/png",
|
|
412
|
+
thumbnails: [
|
|
413
|
+
{
|
|
414
|
+
height: 720,
|
|
415
|
+
width: 1280,
|
|
416
|
+
offsetX: 0,
|
|
417
|
+
offsetY: 0,
|
|
418
|
+
start: 0,
|
|
419
|
+
end: Number.MAX_SAFE_INTEGER,
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
})),
|
|
423
|
+
),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function makeThumbnailContext(
|
|
428
|
+
overrides: { segmentId?: string; trackId?: string; periodId?: string } = {},
|
|
429
|
+
): any {
|
|
430
|
+
return {
|
|
431
|
+
segment: { id: overrides.segmentId ?? "seg-1", time: 0 },
|
|
432
|
+
track: { id: overrides.trackId ?? "track-1", cdnMetadata: [] },
|
|
433
|
+
period: { id: overrides.periodId ?? "period-1" },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { ISegment, IThumbnailTrack } from "../../../manifest";
|
|
|
5
5
|
import type { ICdnMetadata } from "../../../parsers/manifest";
|
|
6
6
|
import type { IPeriod } from "../../../public_types";
|
|
7
7
|
import type {
|
|
8
|
+
IRequestedData,
|
|
8
9
|
IThumbnailLoader,
|
|
9
10
|
IThumbnailLoaderOptions,
|
|
10
11
|
IThumbnailPipeline,
|
|
@@ -46,7 +47,7 @@ export default function createThumbnailFetcher(
|
|
|
46
47
|
|
|
47
48
|
interface IPendingThumbnailRequestInfo {
|
|
48
49
|
/** Promise behind the thumbnail request. */
|
|
49
|
-
promise: Promise<
|
|
50
|
+
promise: Promise<IRequestedData<ArrayBuffer>>;
|
|
50
51
|
/**
|
|
51
52
|
* Multiple caller might share the same request promise.
|
|
52
53
|
* This reference counter keeps track of the number of caller that are
|
|
@@ -86,6 +87,8 @@ export default function createThumbnailFetcher(
|
|
|
86
87
|
): Promise<IThumbnailResponse> {
|
|
87
88
|
cancellationSignal.register(onCancellation);
|
|
88
89
|
|
|
90
|
+
const { segment: thumbnail, track: thumbnailTrack } = thumbnailContext;
|
|
91
|
+
|
|
89
92
|
let currRequestInfo: IPendingThumbnailRequestInfo;
|
|
90
93
|
|
|
91
94
|
// First check if we're not requesting again the last thumbnail
|
|
@@ -113,10 +116,12 @@ export default function createThumbnailFetcher(
|
|
|
113
116
|
throw err;
|
|
114
117
|
}
|
|
115
118
|
cancellationSignal.deregister(onCancellation);
|
|
116
|
-
return response;
|
|
117
|
-
}
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
if (cancellationSignal.isCancelled()) {
|
|
121
|
+
throw cancellationSignal.cancellationError;
|
|
122
|
+
}
|
|
123
|
+
return parseThumbnailStep(response);
|
|
124
|
+
}
|
|
120
125
|
|
|
121
126
|
// NOTE: For now, as multiple `fetchThumbnail` calls might rely on the
|
|
122
127
|
// same request, it is difficult to let different request options
|
|
@@ -144,7 +149,7 @@ export default function createThumbnailFetcher(
|
|
|
144
149
|
* as several calls might share the same request.
|
|
145
150
|
*/
|
|
146
151
|
const requestCanceller = new TaskCanceller("Thumbnail request");
|
|
147
|
-
const fetchPromise =
|
|
152
|
+
const fetchPromise = fetchThumbnailStep();
|
|
148
153
|
currRequestInfo = {
|
|
149
154
|
thumbnailContext,
|
|
150
155
|
promise: fetchPromise,
|
|
@@ -157,37 +162,37 @@ export default function createThumbnailFetcher(
|
|
|
157
162
|
pendingRequestsInfo.splice(currRequestIdx, 1);
|
|
158
163
|
}
|
|
159
164
|
};
|
|
165
|
+
let fetchResult;
|
|
160
166
|
try {
|
|
161
|
-
|
|
162
|
-
clearRequestInfo();
|
|
163
|
-
return fetchResult;
|
|
167
|
+
fetchResult = await fetchPromise;
|
|
164
168
|
} catch (err) {
|
|
165
169
|
clearRequestInfo();
|
|
170
|
+
cancellationSignal.deregister(onCancellation);
|
|
166
171
|
throw err;
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
|
|
174
|
+
clearRequestInfo();
|
|
175
|
+
cancellationSignal.deregister(onCancellation);
|
|
176
|
+
if (cancellationSignal.isCancelled()) {
|
|
177
|
+
throw cancellationSignal.cancellationError;
|
|
178
|
+
}
|
|
179
|
+
return parseThumbnailStep(fetchResult);
|
|
180
|
+
|
|
181
|
+
async function fetchThumbnailStep(): Promise<IRequestedData<ArrayBuffer>> {
|
|
170
182
|
log.debug("Thumbnails", "Beginning thumbnail request", { time: thumbnail.time });
|
|
171
|
-
let res;
|
|
172
183
|
try {
|
|
173
|
-
res = await scheduleRequestWithCdns(
|
|
184
|
+
const res = await scheduleRequestWithCdns(
|
|
174
185
|
thumbnailTrack.cdnMetadata,
|
|
175
186
|
cdnPrioritizer,
|
|
176
187
|
callLoaderWithUrl,
|
|
177
188
|
objectAssign({ onRetry }, requestOptions),
|
|
178
189
|
requestCanceller.signal,
|
|
179
190
|
);
|
|
180
|
-
|
|
181
|
-
if (cancellationSignal.isCancelled()) {
|
|
182
|
-
return Promise.reject(cancellationSignal.cancellationError);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
191
|
log.debug("Thumbnails", "Thumbnail request ended with success", {
|
|
186
192
|
time: thumbnail.time,
|
|
187
193
|
});
|
|
188
|
-
|
|
194
|
+
return res;
|
|
189
195
|
} catch (err) {
|
|
190
|
-
cancellationSignal.deregister(onCancellation);
|
|
191
196
|
if (err instanceof CancellationError) {
|
|
192
197
|
log.debug("Thumbnails", "Thumbnail request aborted", { time: thumbnail.time });
|
|
193
198
|
throw err;
|
|
@@ -195,13 +200,16 @@ export default function createThumbnailFetcher(
|
|
|
195
200
|
log.debug("Thumbnails", "Thumbnail request failed", { time: thumbnail.time });
|
|
196
201
|
throw errorSelector(err);
|
|
197
202
|
}
|
|
203
|
+
}
|
|
198
204
|
|
|
205
|
+
function parseThumbnailStep(
|
|
206
|
+
rawResponse: IRequestedData<ArrayBuffer>,
|
|
207
|
+
): IThumbnailResponse {
|
|
199
208
|
try {
|
|
200
|
-
|
|
209
|
+
return pipeline.parseThumbnail(rawResponse.responseData, {
|
|
201
210
|
thumbnail,
|
|
202
211
|
thumbnailTrack,
|
|
203
212
|
});
|
|
204
|
-
return parsed;
|
|
205
213
|
} catch (error) {
|
|
206
214
|
throw formatError(error, {
|
|
207
215
|
defaultCode: "PIPELINE_PARSE_ERROR",
|