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,846 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
IMediaSource,
|
|
4
|
+
ISourceBuffer,
|
|
5
|
+
ISourceBufferList,
|
|
6
|
+
} from "../../compat/browser_compatibility_types";
|
|
7
|
+
import { SourceBufferError } from "../../errors";
|
|
8
|
+
import noop from "../../utils/noop";
|
|
9
|
+
import MainMediaSourceInterface, {
|
|
10
|
+
MainSourceBufferInterface,
|
|
11
|
+
} from "../main_media_source_interface";
|
|
12
|
+
import { SourceBufferType } from "../types";
|
|
13
|
+
import MediaSourceDurationUpdater from "../utils/media_source_duration_updater";
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
MockMediaSource,
|
|
17
|
+
MockSourceBuffer,
|
|
18
|
+
getLastMediaSourceMock,
|
|
19
|
+
mediaSourceProps,
|
|
20
|
+
makeMockSourceBufferList,
|
|
21
|
+
mockMaintainEndOfStream,
|
|
22
|
+
mockOnSourceClose,
|
|
23
|
+
mockOnSourceEnded,
|
|
24
|
+
mockOnSourceOpen,
|
|
25
|
+
mockTryToChangeSourceBufferType,
|
|
26
|
+
} = vi.hoisted(() => {
|
|
27
|
+
/** Creates a minimal mock SourceBuffer with controllable event dispatching */
|
|
28
|
+
class InnerMockSourceBuffer implements ISourceBuffer {
|
|
29
|
+
private _listeners: Record<string, Array<(e?: unknown) => void>> = {};
|
|
30
|
+
updating: boolean = false;
|
|
31
|
+
buffered: TimeRanges = {
|
|
32
|
+
length: 0,
|
|
33
|
+
start: vi.fn().mockReturnValue(0),
|
|
34
|
+
end: vi.fn().mockReturnValue(0),
|
|
35
|
+
};
|
|
36
|
+
timestampOffset: number = 0;
|
|
37
|
+
appendWindowStart: number = 0;
|
|
38
|
+
appendWindowEnd: number = Infinity;
|
|
39
|
+
onabort = null;
|
|
40
|
+
onerror = null;
|
|
41
|
+
onupdate = null;
|
|
42
|
+
onupdateend = null;
|
|
43
|
+
onupdatestart = null;
|
|
44
|
+
changeType(): void {
|
|
45
|
+
// noop
|
|
46
|
+
}
|
|
47
|
+
appendBuffer(): void {
|
|
48
|
+
// noop
|
|
49
|
+
}
|
|
50
|
+
remove(): void {
|
|
51
|
+
// noop
|
|
52
|
+
}
|
|
53
|
+
abort(): void {
|
|
54
|
+
// noop
|
|
55
|
+
}
|
|
56
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
57
|
+
addEventListener(event: any, cb: (e?: any) => void) {
|
|
58
|
+
this._listeners[event] = this._listeners[event] ?? [];
|
|
59
|
+
this._listeners[event].push(cb);
|
|
60
|
+
}
|
|
61
|
+
removeEventListener(event?: any, cb?: ((e?: any) => void) | undefined) {
|
|
62
|
+
if (event === undefined) {
|
|
63
|
+
this._listeners = {};
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this._listeners[event] = (this._listeners[event] ?? []).filter((fn) =>
|
|
67
|
+
cb === undefined ? false : fn !== cb,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
72
|
+
_emit(event: string, arg?: unknown) {
|
|
73
|
+
for (const cb of this._listeners[event] ?? []) {
|
|
74
|
+
cb(arg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a minimal ISourceBufferList-compatible object from an array of
|
|
81
|
+
* ISourceBuffer instances.
|
|
82
|
+
*/
|
|
83
|
+
function makeMockSourceBufferListInner(buffers: ISourceBuffer[]): ISourceBufferList {
|
|
84
|
+
return {
|
|
85
|
+
onaddsourcebuffer: null,
|
|
86
|
+
onremovesourcebuffer: null,
|
|
87
|
+
get length() {
|
|
88
|
+
return buffers.length;
|
|
89
|
+
},
|
|
90
|
+
addEventListener: vi.fn(),
|
|
91
|
+
removeEventListener: vi.fn(),
|
|
92
|
+
...Object.fromEntries(buffers.map((buf, i) => [i, buf])),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const mediaSourcePropsObj: {
|
|
97
|
+
handle: MediaProvider | undefined;
|
|
98
|
+
streaming: boolean | undefined;
|
|
99
|
+
} = { handle: undefined, streaming: undefined };
|
|
100
|
+
|
|
101
|
+
let lastCreatedMockMS: InnerMockMediaSource | null = null;
|
|
102
|
+
class InnerMockMediaSource implements IMediaSource {
|
|
103
|
+
private _mediaSourceListeners: Record<string, Array<() => void>>;
|
|
104
|
+
readyState: ReadyState = "open";
|
|
105
|
+
duration: number = 0;
|
|
106
|
+
onsourceclose = null;
|
|
107
|
+
onsourceended = null;
|
|
108
|
+
onsourceopen = null;
|
|
109
|
+
constructor() {
|
|
110
|
+
this._mediaSourceListeners = {};
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
112
|
+
lastCreatedMockMS = this;
|
|
113
|
+
}
|
|
114
|
+
clearLiveSeekableRange(): never {
|
|
115
|
+
throw new Error("not implemented");
|
|
116
|
+
}
|
|
117
|
+
setLiveSeekableRange(): never {
|
|
118
|
+
throw new Error("not implemented");
|
|
119
|
+
}
|
|
120
|
+
sourceBuffers = makeMockSourceBufferListInner([]);
|
|
121
|
+
addSourceBuffer(): ISourceBuffer {
|
|
122
|
+
throw new Error("not implemented");
|
|
123
|
+
}
|
|
124
|
+
removeSourceBuffer(): never {
|
|
125
|
+
throw new Error("not implemented");
|
|
126
|
+
}
|
|
127
|
+
endOfStream(): void {
|
|
128
|
+
// noop
|
|
129
|
+
}
|
|
130
|
+
get handle(): MediaProvider | undefined {
|
|
131
|
+
return mediaSourcePropsObj.handle;
|
|
132
|
+
}
|
|
133
|
+
get streaming(): boolean | undefined {
|
|
134
|
+
return mediaSourcePropsObj.streaming;
|
|
135
|
+
}
|
|
136
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
137
|
+
addEventListener(event: any, cb: (e?: any) => void) {
|
|
138
|
+
this._mediaSourceListeners[event] = this._mediaSourceListeners[event] ?? [];
|
|
139
|
+
this._mediaSourceListeners[event].push(cb);
|
|
140
|
+
}
|
|
141
|
+
removeEventListener(event?: any, cb?: ((e?: any) => void) | undefined) {
|
|
142
|
+
if (event === undefined) {
|
|
143
|
+
this._mediaSourceListeners = {};
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this._mediaSourceListeners[event] = (
|
|
147
|
+
this._mediaSourceListeners[event] ?? []
|
|
148
|
+
).filter((fn) => (cb === undefined ? false : fn !== cb));
|
|
149
|
+
}
|
|
150
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
151
|
+
|
|
152
|
+
_emit(event: string) {
|
|
153
|
+
for (const cb of this._mediaSourceListeners[event] ?? []) {
|
|
154
|
+
cb();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
mockTryToChangeSourceBufferType: vi.fn().mockReturnValue(true),
|
|
161
|
+
mockOnSourceOpen: vi.fn(),
|
|
162
|
+
mockOnSourceEnded: vi.fn(),
|
|
163
|
+
mockOnSourceClose: vi.fn(),
|
|
164
|
+
mockMaintainEndOfStream: vi.fn(),
|
|
165
|
+
MockMediaSource: InnerMockMediaSource,
|
|
166
|
+
MockSourceBuffer: InnerMockSourceBuffer,
|
|
167
|
+
makeMockSourceBufferList: makeMockSourceBufferListInner,
|
|
168
|
+
mediaSourceProps: mediaSourcePropsObj,
|
|
169
|
+
getLastMediaSourceMock: (): InnerMockMediaSource => {
|
|
170
|
+
if (lastCreatedMockMS === null) {
|
|
171
|
+
throw new Error("No MockMediaSource has been instantiated yet");
|
|
172
|
+
}
|
|
173
|
+
return lastCreatedMockMS;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
vi.mock("../../compat/browser_compatibility_types", () => {
|
|
179
|
+
return {
|
|
180
|
+
MediaSource_: MockMediaSource,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
vi.mock("../../compat/change_source_buffer_type", () => ({
|
|
185
|
+
default: mockTryToChangeSourceBufferType,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
vi.mock("../../compat/event_listeners", () => ({
|
|
189
|
+
onSourceOpen: mockOnSourceOpen,
|
|
190
|
+
onSourceEnded: mockOnSourceEnded,
|
|
191
|
+
onSourceClose: mockOnSourceClose,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
vi.mock("../../log", () => ({
|
|
195
|
+
default: {
|
|
196
|
+
info: vi.fn(),
|
|
197
|
+
debug: vi.fn(),
|
|
198
|
+
error: vi.fn(),
|
|
199
|
+
warn: vi.fn(),
|
|
200
|
+
},
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
vi.mock("../utils/end_of_stream", () => ({
|
|
204
|
+
maintainEndOfStream: mockMaintainEndOfStream,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
describe("MainMediaSourceInterface", () => {
|
|
208
|
+
const mockUpdateDuration = vi.spyOn(
|
|
209
|
+
MediaSourceDurationUpdater.prototype,
|
|
210
|
+
"updateDuration",
|
|
211
|
+
);
|
|
212
|
+
const mockStopUpdating = vi.spyOn(MediaSourceDurationUpdater.prototype, "stopUpdating");
|
|
213
|
+
|
|
214
|
+
let mockMS: InstanceType<typeof MockMediaSource>;
|
|
215
|
+
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
mockOnSourceOpen.mockReset();
|
|
218
|
+
mockOnSourceEnded.mockReset();
|
|
219
|
+
mockOnSourceClose.mockReset();
|
|
220
|
+
mockUpdateDuration.mockReset();
|
|
221
|
+
mockStopUpdating.mockReset();
|
|
222
|
+
mockMaintainEndOfStream.mockReset();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
mediaSourceProps.handle = undefined;
|
|
227
|
+
mediaSourceProps.streaming = undefined;
|
|
228
|
+
vi.resetAllMocks();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("constructs correctly and registers event listeners", () => {
|
|
232
|
+
const iface = new MainMediaSourceInterface("test-id");
|
|
233
|
+
expect(iface.id).toBe("test-id");
|
|
234
|
+
expect(iface.sourceBuffers).toEqual([]);
|
|
235
|
+
expect(iface.readyState).toBe("open");
|
|
236
|
+
expect(mockOnSourceOpen).toHaveBeenCalledOnce();
|
|
237
|
+
expect(mockOnSourceEnded).toHaveBeenCalledOnce();
|
|
238
|
+
expect(mockOnSourceClose).toHaveBeenCalledOnce();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("uses forcedMediaSource when provided", () => {
|
|
242
|
+
const error = new Error("toto");
|
|
243
|
+
class ExtentedMediaSource extends MockMediaSource {
|
|
244
|
+
addSourceBuffer(): ISourceBuffer {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const ForcedConstructor = ExtentedMediaSource;
|
|
249
|
+
const iface = new MainMediaSourceInterface("forced-id", ForcedConstructor);
|
|
250
|
+
expect(() => iface.addSourceBuffer(SourceBufferType.Video, "test")).toThrow(error);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("sets handle to media-source type when mediaSource.handle is nullish", () => {
|
|
254
|
+
mediaSourceProps.handle = undefined;
|
|
255
|
+
const iface = new MainMediaSourceInterface("h-id");
|
|
256
|
+
expect(iface.handle).toMatchObject({ type: "media-source" });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("sets handle to 'handle' type when mediaSource.handle is defined", () => {
|
|
260
|
+
const fakeHandle = new Blob();
|
|
261
|
+
mediaSourceProps.handle = fakeHandle;
|
|
262
|
+
const iface = new MainMediaSourceInterface("h-id");
|
|
263
|
+
expect(iface.handle).toMatchObject({ type: "handle", value: fakeHandle });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("tracks streaming from the MediaSource when it is defined", () => {
|
|
267
|
+
mediaSourceProps.streaming = true;
|
|
268
|
+
const iface1 = new MainMediaSourceInterface("stream-id");
|
|
269
|
+
expect(iface1.streaming).toBe(true);
|
|
270
|
+
mediaSourceProps.streaming = false;
|
|
271
|
+
const iface2 = new MainMediaSourceInterface("stream-id");
|
|
272
|
+
expect(iface2.streaming).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("updates streaming to true when 'startstreaming' fires", () => {
|
|
276
|
+
mediaSourceProps.streaming = false;
|
|
277
|
+
const iface = new MainMediaSourceInterface("stream-id");
|
|
278
|
+
mockMS = getLastMediaSourceMock();
|
|
279
|
+
mockMS._emit("startstreaming");
|
|
280
|
+
expect(iface.streaming).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("updates streaming to false when 'endstreaming' fires", () => {
|
|
284
|
+
mediaSourceProps.streaming = true;
|
|
285
|
+
const iface = new MainMediaSourceInterface("stream-id");
|
|
286
|
+
mockMS = getLastMediaSourceMock();
|
|
287
|
+
mockMS._emit("endstreaming");
|
|
288
|
+
expect(iface.streaming).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("triggers mediaSourceOpen when onSourceOpen callback fires", () => {
|
|
292
|
+
const iface = new MainMediaSourceInterface("open-id");
|
|
293
|
+
const listener = vi.fn();
|
|
294
|
+
iface.addEventListener("mediaSourceOpen", listener);
|
|
295
|
+
const [[, cb]] = mockOnSourceOpen.mock.calls;
|
|
296
|
+
cb();
|
|
297
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("triggers mediaSourceEnded when onSourceEnded callback fires", () => {
|
|
301
|
+
const iface = new MainMediaSourceInterface("ended-id");
|
|
302
|
+
const listener = vi.fn();
|
|
303
|
+
iface.addEventListener("mediaSourceEnded", listener);
|
|
304
|
+
const [[, cb]] = mockOnSourceEnded.mock.calls;
|
|
305
|
+
cb();
|
|
306
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("triggers mediaSourceClose when onSourceClose callback fires", () => {
|
|
310
|
+
const iface = new MainMediaSourceInterface("close-id");
|
|
311
|
+
const listener = vi.fn();
|
|
312
|
+
iface.addEventListener("mediaSourceClose", listener);
|
|
313
|
+
const [[, cb]] = mockOnSourceClose.mock.calls;
|
|
314
|
+
cb();
|
|
315
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("addSourceBuffer creates and stores a MainSourceBufferInterface", () => {
|
|
319
|
+
const sb = new MockSourceBuffer();
|
|
320
|
+
vi.spyOn(MockMediaSource.prototype, "addSourceBuffer").mockReturnValue(sb);
|
|
321
|
+
const iface = new MainMediaSourceInterface("sb-id");
|
|
322
|
+
const result = iface.addSourceBuffer(
|
|
323
|
+
SourceBufferType.Video,
|
|
324
|
+
"video/mp4; codecs=avc1",
|
|
325
|
+
);
|
|
326
|
+
expect(result).toBeInstanceOf(MainSourceBufferInterface);
|
|
327
|
+
expect(iface.sourceBuffers).toHaveLength(1);
|
|
328
|
+
expect(iface.sourceBuffers[0]).toBe(result);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("setDuration delegates to durationUpdater", () => {
|
|
332
|
+
const iface = new MainMediaSourceInterface("dur-id");
|
|
333
|
+
iface.setDuration(120, true);
|
|
334
|
+
expect(mockUpdateDuration).toHaveBeenCalledWith(120, true);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("interruptDurationSetting delegates to durationUpdater", () => {
|
|
338
|
+
const iface = new MainMediaSourceInterface("dur-id");
|
|
339
|
+
iface.interruptDurationSetting("test-reason");
|
|
340
|
+
expect(mockStopUpdating).toHaveBeenCalledWith("test-reason");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("maintainEndOfStream calls maintainEndOfStream util only once", () => {
|
|
344
|
+
const iface = new MainMediaSourceInterface("eos-id");
|
|
345
|
+
iface.maintainEndOfStream();
|
|
346
|
+
iface.maintainEndOfStream(); // second call should not create another canceller
|
|
347
|
+
expect(mockMaintainEndOfStream).toHaveBeenCalledOnce();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("stopEndOfStream cancels active end-of-stream", () => {
|
|
351
|
+
const iface = new MainMediaSourceInterface("eos-id");
|
|
352
|
+
iface.maintainEndOfStream();
|
|
353
|
+
expect(mockMaintainEndOfStream).toHaveBeenCalledOnce();
|
|
354
|
+
iface.stopEndOfStream();
|
|
355
|
+
// Calling again should be a no-op (no second cancel call means no throw)
|
|
356
|
+
iface.stopEndOfStream();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("dispose cleans up sourceBuffers and resets media source", () => {
|
|
360
|
+
const sb = new MockSourceBuffer();
|
|
361
|
+
const mockAbort = vi.spyOn(sb, "abort");
|
|
362
|
+
vi.spyOn(MockMediaSource.prototype, "addSourceBuffer").mockReturnValue(sb);
|
|
363
|
+
const iface = new MainMediaSourceInterface("dispose-id");
|
|
364
|
+
mockMS = getLastMediaSourceMock();
|
|
365
|
+
mockMS.sourceBuffers = makeMockSourceBufferList([sb]);
|
|
366
|
+
iface.addSourceBuffer(SourceBufferType.Audio, "audio/mp4; codecs=mp4a");
|
|
367
|
+
iface.dispose("test-dispose");
|
|
368
|
+
expect(mockAbort).toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("MainSourceBufferInterface", () => {
|
|
373
|
+
let sb: ISourceBuffer & {
|
|
374
|
+
_emit(event: string, arg?: unknown): void;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
beforeEach(() => {
|
|
378
|
+
sb = new MockSourceBuffer();
|
|
379
|
+
mockTryToChangeSourceBufferType.mockReturnValue(true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
afterEach(() => {
|
|
383
|
+
vi.resetAllMocks();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
function createMainSourceBufferInterface(
|
|
387
|
+
type = SourceBufferType.Video,
|
|
388
|
+
codec = "video/mp4",
|
|
389
|
+
) {
|
|
390
|
+
return new MainSourceBufferInterface(type, codec, sb);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
it("appendBuffer returns a promise that resolves on updateend", async () => {
|
|
394
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
395
|
+
const iface = createMainSourceBufferInterface();
|
|
396
|
+
const data = new Uint8Array([1, 2, 3]);
|
|
397
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
398
|
+
sb._emit("updateend");
|
|
399
|
+
const result = await promise;
|
|
400
|
+
expect(Array.isArray(result)).toBe(true);
|
|
401
|
+
expect(mockAppendBuffer).toHaveBeenCalledWith(data);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("appendBuffer rejects on SourceBuffer error event", async () => {
|
|
405
|
+
const iface = createMainSourceBufferInterface();
|
|
406
|
+
const data = new Uint8Array([1]);
|
|
407
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
408
|
+
const err = new Error("Some Message");
|
|
409
|
+
err.name = "SomeError";
|
|
410
|
+
sb._emit("error", err);
|
|
411
|
+
const thrownError = new SourceBufferError("SomeError", "Some Message", false);
|
|
412
|
+
await expect(promise).rejects.toMatchObject(thrownError);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("appendBuffer rejects with QuotaExceededError flagged correctly", async () => {
|
|
416
|
+
const iface = createMainSourceBufferInterface();
|
|
417
|
+
const data = new Uint8Array([1]);
|
|
418
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
419
|
+
const err = new Error("QuotaExceeded");
|
|
420
|
+
err.name = "QuotaExceededError";
|
|
421
|
+
sb._emit("error", err);
|
|
422
|
+
const thrownError = new SourceBufferError(
|
|
423
|
+
"QuotaExceededError",
|
|
424
|
+
"QuotaExceeded",
|
|
425
|
+
true,
|
|
426
|
+
);
|
|
427
|
+
await expect(promise).rejects.toMatchObject(thrownError);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("updates codec via tryToChangeSourceBufferType when a different codec is passed", async () => {
|
|
431
|
+
const iface = createMainSourceBufferInterface(SourceBufferType.Video, "video/mp4");
|
|
432
|
+
const data = new Uint8Array([1]);
|
|
433
|
+
const promise = iface.appendBuffer(data, { codec: "video/webm" });
|
|
434
|
+
sb._emit("updateend");
|
|
435
|
+
await promise;
|
|
436
|
+
expect(mockTryToChangeSourceBufferType).toHaveBeenCalledWith(sb, "video/webm");
|
|
437
|
+
expect(iface.codec).toBe("video/webm");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("does not update codec when tryToChangeSourceBufferType returns false", async () => {
|
|
441
|
+
mockTryToChangeSourceBufferType.mockReturnValue(false);
|
|
442
|
+
const iface = createMainSourceBufferInterface(SourceBufferType.Video, "video/mp4");
|
|
443
|
+
const data = new Uint8Array([1]);
|
|
444
|
+
const promise = iface.appendBuffer(data, { codec: "video/webm" });
|
|
445
|
+
sb._emit("updateend");
|
|
446
|
+
await promise;
|
|
447
|
+
expect(iface.codec).toBe("video/mp4");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("sets timestampOffset when it differs from current value", async () => {
|
|
451
|
+
const iface = createMainSourceBufferInterface();
|
|
452
|
+
sb.timestampOffset = 0;
|
|
453
|
+
const data = new Uint8Array([1]);
|
|
454
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4", timestampOffset: 5 });
|
|
455
|
+
sb._emit("updateend");
|
|
456
|
+
await promise;
|
|
457
|
+
expect(sb.timestampOffset).toBe(5);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("does not set timestampOffset when it matches current value", async () => {
|
|
461
|
+
const iface = createMainSourceBufferInterface();
|
|
462
|
+
sb.timestampOffset = 5;
|
|
463
|
+
const data = new Uint8Array([1]);
|
|
464
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4", timestampOffset: 5 });
|
|
465
|
+
sb._emit("updateend");
|
|
466
|
+
await promise;
|
|
467
|
+
expect(sb.timestampOffset).toBe(5);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("resets appendWindowStart to 0 when appendWindow[0] is undefined and was > 0", async () => {
|
|
471
|
+
const iface = createMainSourceBufferInterface();
|
|
472
|
+
sb.appendWindowStart = 2;
|
|
473
|
+
const data = new Uint8Array([1]);
|
|
474
|
+
const promise = iface.appendBuffer(data, {
|
|
475
|
+
codec: "video/mp4",
|
|
476
|
+
appendWindow: [undefined, undefined],
|
|
477
|
+
});
|
|
478
|
+
sb._emit("updateend");
|
|
479
|
+
await promise;
|
|
480
|
+
expect(sb.appendWindowStart).toBe(0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("sets appendWindowStart when it differs", async () => {
|
|
484
|
+
const iface = createMainSourceBufferInterface();
|
|
485
|
+
sb.appendWindowStart = 0;
|
|
486
|
+
sb.appendWindowEnd = Infinity;
|
|
487
|
+
const data = new Uint8Array([1]);
|
|
488
|
+
const promise = iface.appendBuffer(data, {
|
|
489
|
+
codec: "video/mp4",
|
|
490
|
+
appendWindow: [3, 10],
|
|
491
|
+
});
|
|
492
|
+
sb._emit("updateend");
|
|
493
|
+
await promise;
|
|
494
|
+
expect(sb.appendWindowStart).toBe(3);
|
|
495
|
+
expect(sb.appendWindowEnd).toBe(10);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("pre-updates appendWindowEnd before setting appendWindowStart when needed", async () => {
|
|
499
|
+
const iface = createMainSourceBufferInterface();
|
|
500
|
+
sb.appendWindowStart = 0;
|
|
501
|
+
sb.appendWindowEnd = 5; // new start (8) >= current end (5) → must bump end first
|
|
502
|
+
const data = new Uint8Array([1]);
|
|
503
|
+
const promise = iface.appendBuffer(data, {
|
|
504
|
+
codec: "video/mp4",
|
|
505
|
+
appendWindow: [8, 20],
|
|
506
|
+
});
|
|
507
|
+
sb._emit("updateend");
|
|
508
|
+
await promise;
|
|
509
|
+
expect(sb.appendWindowStart).toBe(8);
|
|
510
|
+
expect(sb.appendWindowEnd).toBe(20);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("resets appendWindowEnd to Infinity when appendWindow[1] is undefined and was not Infinity", async () => {
|
|
514
|
+
const iface = createMainSourceBufferInterface();
|
|
515
|
+
sb.appendWindowEnd = 30;
|
|
516
|
+
const data = new Uint8Array([1]);
|
|
517
|
+
const promise = iface.appendBuffer(data, {
|
|
518
|
+
codec: "video/mp4",
|
|
519
|
+
appendWindow: [undefined, undefined],
|
|
520
|
+
});
|
|
521
|
+
sb._emit("updateend");
|
|
522
|
+
await promise;
|
|
523
|
+
expect(sb.appendWindowEnd).toBe(Infinity);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("merges consecutive Push operations with identical parameters", async () => {
|
|
527
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
528
|
+
const iface = createMainSourceBufferInterface();
|
|
529
|
+
const data1 = new Uint8Array([1, 2]);
|
|
530
|
+
const data2 = new Uint8Array([3, 4]);
|
|
531
|
+
|
|
532
|
+
// First block all appendBuffer operation by implying that the sourcebuffer is updating
|
|
533
|
+
sb.updating = true;
|
|
534
|
+
const p1 = iface.appendBuffer(data1, { codec: "video/mp4", timestampOffset: 0 });
|
|
535
|
+
const p2 = iface.appendBuffer(data2, { codec: "video/mp4", timestampOffset: 0 });
|
|
536
|
+
|
|
537
|
+
expect(mockAppendBuffer).not.toHaveBeenCalled();
|
|
538
|
+
|
|
539
|
+
// Remove lock
|
|
540
|
+
sb.updating = false;
|
|
541
|
+
sb._emit("updateend");
|
|
542
|
+
sb.updating = true;
|
|
543
|
+
|
|
544
|
+
// Both segments are merged into a single appendBuffer call
|
|
545
|
+
expect(mockAppendBuffer).toHaveBeenCalledOnce();
|
|
546
|
+
|
|
547
|
+
// One updateend resolves both promises
|
|
548
|
+
sb.updating = false;
|
|
549
|
+
sb._emit("updateend");
|
|
550
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
551
|
+
expect(r1).toEqual(r2);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("does NOT merge consecutive Push operations with different codecs", async () => {
|
|
555
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
556
|
+
const iface = createMainSourceBufferInterface(SourceBufferType.Video, "video/mp4");
|
|
557
|
+
const data1 = new Uint8Array([1]);
|
|
558
|
+
const data2 = new Uint8Array([2]);
|
|
559
|
+
|
|
560
|
+
// First block all appendBuffer operation by implying that the sourcebuffer is updating
|
|
561
|
+
sb.updating = true;
|
|
562
|
+
|
|
563
|
+
const p1 = iface.appendBuffer(data1, { codec: "video/mp4" });
|
|
564
|
+
const p2 = iface.appendBuffer(data2, { codec: "video/webm" });
|
|
565
|
+
|
|
566
|
+
expect(mockAppendBuffer).not.toHaveBeenCalled();
|
|
567
|
+
|
|
568
|
+
// Remove lock
|
|
569
|
+
sb.updating = false;
|
|
570
|
+
sb._emit("updateend");
|
|
571
|
+
sb.updating = true;
|
|
572
|
+
|
|
573
|
+
// First push executed immediately
|
|
574
|
+
expect(mockAppendBuffer).toHaveBeenCalledOnce();
|
|
575
|
+
|
|
576
|
+
sb.updating = false;
|
|
577
|
+
sb._emit("updateend");
|
|
578
|
+
sb.updating = true;
|
|
579
|
+
await p1;
|
|
580
|
+
|
|
581
|
+
// Second push should start now
|
|
582
|
+
sb.updating = false;
|
|
583
|
+
sb._emit("updateend");
|
|
584
|
+
sb.updating = true;
|
|
585
|
+
await p2;
|
|
586
|
+
|
|
587
|
+
expect(mockAppendBuffer).toHaveBeenCalledTimes(2);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("does NOT merge consecutive Push operations with different timestampOffsets", async () => {
|
|
591
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
592
|
+
const iface = createMainSourceBufferInterface();
|
|
593
|
+
const data1 = new Uint8Array([1]);
|
|
594
|
+
const data2 = new Uint8Array([2]);
|
|
595
|
+
|
|
596
|
+
// First block all appendBuffer operation by implying that the sourcebuffer is updating
|
|
597
|
+
sb.updating = true;
|
|
598
|
+
|
|
599
|
+
const p1 = iface.appendBuffer(data1, { codec: "video/mp4", timestampOffset: 0 });
|
|
600
|
+
const p2 = iface.appendBuffer(data2, { codec: "video/mp4", timestampOffset: 5 });
|
|
601
|
+
|
|
602
|
+
expect(mockAppendBuffer).not.toHaveBeenCalled();
|
|
603
|
+
|
|
604
|
+
// Remove lock
|
|
605
|
+
sb.updating = false;
|
|
606
|
+
sb._emit("updateend");
|
|
607
|
+
sb.updating = true;
|
|
608
|
+
|
|
609
|
+
expect(mockAppendBuffer).toHaveBeenCalledOnce();
|
|
610
|
+
|
|
611
|
+
sb.updating = false;
|
|
612
|
+
sb._emit("updateend");
|
|
613
|
+
sb.updating = true;
|
|
614
|
+
await p1;
|
|
615
|
+
|
|
616
|
+
sb.updating = false;
|
|
617
|
+
sb._emit("updateend");
|
|
618
|
+
sb.updating = true;
|
|
619
|
+
await p2;
|
|
620
|
+
|
|
621
|
+
expect(mockAppendBuffer).toHaveBeenCalledTimes(2);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("handles ArrayBuffer input in merge path", async () => {
|
|
625
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
626
|
+
const iface = createMainSourceBufferInterface();
|
|
627
|
+
const buf1 = new ArrayBuffer(2);
|
|
628
|
+
new Uint8Array(buf1).set([1, 2]);
|
|
629
|
+
const buf2 = new ArrayBuffer(2);
|
|
630
|
+
new Uint8Array(buf2).set([3, 4]);
|
|
631
|
+
|
|
632
|
+
// First block all appendBuffer operation by implying that the sourcebuffer is updating
|
|
633
|
+
sb.updating = true;
|
|
634
|
+
|
|
635
|
+
const p1 = iface.appendBuffer(buf1, { codec: "video/mp4" });
|
|
636
|
+
const p2 = iface.appendBuffer(buf2, { codec: "video/mp4" });
|
|
637
|
+
|
|
638
|
+
expect(mockAppendBuffer).not.toHaveBeenCalled();
|
|
639
|
+
|
|
640
|
+
// Remove lock
|
|
641
|
+
sb.updating = false;
|
|
642
|
+
sb._emit("updateend");
|
|
643
|
+
sb.updating = true;
|
|
644
|
+
|
|
645
|
+
expect(mockAppendBuffer).toHaveBeenCalledOnce();
|
|
646
|
+
sb.updating = false;
|
|
647
|
+
sb._emit("updateend");
|
|
648
|
+
await Promise.all([p1, p2]);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("handles DataView input in merge path", async () => {
|
|
652
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
653
|
+
const iface = createMainSourceBufferInterface();
|
|
654
|
+
const buf = new ArrayBuffer(4);
|
|
655
|
+
const dv1 = new DataView(buf, 0, 2);
|
|
656
|
+
const dv2 = new DataView(buf, 2, 2);
|
|
657
|
+
|
|
658
|
+
// First block all appendBuffer operation by implying that the sourcebuffer is updating
|
|
659
|
+
sb.updating = true;
|
|
660
|
+
|
|
661
|
+
const p1 = iface.appendBuffer(dv1, { codec: "video/mp4" });
|
|
662
|
+
const p2 = iface.appendBuffer(dv2, { codec: "video/mp4" });
|
|
663
|
+
|
|
664
|
+
// Remove lock
|
|
665
|
+
sb.updating = false;
|
|
666
|
+
sb._emit("updateend");
|
|
667
|
+
sb.updating = true;
|
|
668
|
+
|
|
669
|
+
expect(mockAppendBuffer).toHaveBeenCalledOnce();
|
|
670
|
+
sb.updating = false;
|
|
671
|
+
sb._emit("updateend");
|
|
672
|
+
await Promise.all([p1, p2]);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("rejects and continues queue when appendBuffer throws synchronously", async () => {
|
|
676
|
+
const iface = createMainSourceBufferInterface();
|
|
677
|
+
vi.spyOn(sb, "appendBuffer").mockImplementationOnce(() => {
|
|
678
|
+
throw new Error("random error");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const data1 = new Uint8Array([1]);
|
|
682
|
+
const data2 = new Uint8Array([2]);
|
|
683
|
+
|
|
684
|
+
const p1 = iface.appendBuffer(data1, { codec: "video/mp4" });
|
|
685
|
+
const p2 = iface.appendBuffer(data2, { codec: "video/mp4" });
|
|
686
|
+
|
|
687
|
+
const error = new SourceBufferError("Error", "random error", false);
|
|
688
|
+
await expect(p1).rejects.toMatchObject(error);
|
|
689
|
+
|
|
690
|
+
// Queue should move on – second push should now run
|
|
691
|
+
// TODO: For a synchronous error, maybe we shouldn't await an updateend?
|
|
692
|
+
sb._emit("updateend");
|
|
693
|
+
await expect(p2).resolves.toBeDefined();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("remove returns a promise that resolves on updateend", async () => {
|
|
697
|
+
const mockRemove = vi.spyOn(MockSourceBuffer.prototype, "remove");
|
|
698
|
+
const iface = createMainSourceBufferInterface();
|
|
699
|
+
const promise = iface.remove(0, 10);
|
|
700
|
+
expect(mockRemove).toHaveBeenCalledWith(0, 10);
|
|
701
|
+
sb._emit("updateend");
|
|
702
|
+
await expect(promise).resolves.toEqual([]);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("remove rejects when SourceBuffer.remove throws synchronously", async () => {
|
|
706
|
+
const iface = createMainSourceBufferInterface();
|
|
707
|
+
vi.spyOn(sb, "remove").mockImplementationOnce(() => {
|
|
708
|
+
throw new Error("some remove error");
|
|
709
|
+
});
|
|
710
|
+
const promise = iface.remove(0, 5);
|
|
711
|
+
const error = new SourceBufferError("Error", "some remove error", false);
|
|
712
|
+
await expect(promise).rejects.toMatchObject(error);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("queues a remove after a pending push", async () => {
|
|
716
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
717
|
+
const mockRemove = vi.spyOn(MockSourceBuffer.prototype, "remove");
|
|
718
|
+
const iface = createMainSourceBufferInterface();
|
|
719
|
+
const data = new Uint8Array([1]);
|
|
720
|
+
|
|
721
|
+
const pushPromise = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
722
|
+
const removePromise = iface.remove(0, 5);
|
|
723
|
+
|
|
724
|
+
// Only the push should have started
|
|
725
|
+
expect(mockAppendBuffer).toHaveBeenCalledOnce();
|
|
726
|
+
expect(mockRemove).not.toHaveBeenCalled();
|
|
727
|
+
|
|
728
|
+
sb._emit("updateend");
|
|
729
|
+
await pushPromise;
|
|
730
|
+
|
|
731
|
+
expect(mockRemove).toHaveBeenCalledWith(0, 5);
|
|
732
|
+
sb._emit("updateend");
|
|
733
|
+
await expect(removePromise).resolves.toBeDefined();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("getBuffered returns ranges from the underlying SourceBuffer", () => {
|
|
737
|
+
const iface = createMainSourceBufferInterface();
|
|
738
|
+
const result = iface.getBuffered();
|
|
739
|
+
expect(Array.isArray(result)).toBe(true);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("getBuffered returns empty array when accessing buffered throws", () => {
|
|
743
|
+
const iface = createMainSourceBufferInterface();
|
|
744
|
+
Object.defineProperty(sb, "buffered", {
|
|
745
|
+
get() {
|
|
746
|
+
throw new Error("buffered unavailable");
|
|
747
|
+
},
|
|
748
|
+
configurable: true,
|
|
749
|
+
});
|
|
750
|
+
const result = iface.getBuffered();
|
|
751
|
+
expect(result).toEqual([]);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("abort calls sourceBuffer.abort and rejects pending operations", async () => {
|
|
755
|
+
const mockAbort = vi.spyOn(MockSourceBuffer.prototype, "abort");
|
|
756
|
+
const iface = createMainSourceBufferInterface();
|
|
757
|
+
const data = new Uint8Array([1]);
|
|
758
|
+
|
|
759
|
+
const p1 = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
760
|
+
const p2 = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
761
|
+
|
|
762
|
+
iface.abort("test-abort");
|
|
763
|
+
|
|
764
|
+
await expect(p1).rejects.toBeDefined();
|
|
765
|
+
await expect(p2).rejects.toBeDefined();
|
|
766
|
+
expect(mockAbort).toHaveBeenCalled();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("dispose calls sourceBuffer.abort and rejects pending operations", async () => {
|
|
770
|
+
const mockAbort = vi.spyOn(MockSourceBuffer.prototype, "abort");
|
|
771
|
+
const iface = createMainSourceBufferInterface();
|
|
772
|
+
const data = new Uint8Array([1]);
|
|
773
|
+
const p1 = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
774
|
+
|
|
775
|
+
iface.dispose("test-dispose");
|
|
776
|
+
|
|
777
|
+
await expect(p1).rejects.toBeDefined();
|
|
778
|
+
expect(mockAbort).toHaveBeenCalled();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("abort does not throw even if sourceBuffer.abort throws", () => {
|
|
782
|
+
const iface = createMainSourceBufferInterface();
|
|
783
|
+
vi.spyOn(sb, "abort").mockImplementation(() => {
|
|
784
|
+
throw new Error("abort threw");
|
|
785
|
+
});
|
|
786
|
+
expect(() => iface.abort("safe")).not.toThrow();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("resolves with empty array when buffered throws InvalidStateError on updateend", async () => {
|
|
790
|
+
const iface = createMainSourceBufferInterface();
|
|
791
|
+
const data = new Uint8Array([1]);
|
|
792
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
793
|
+
|
|
794
|
+
let callCount = 0;
|
|
795
|
+
Object.defineProperty(sb, "buffered", {
|
|
796
|
+
get() {
|
|
797
|
+
callCount++;
|
|
798
|
+
if (callCount > 1) {
|
|
799
|
+
const e = new Error("InvalidStateError");
|
|
800
|
+
e.name = "InvalidStateError";
|
|
801
|
+
throw e;
|
|
802
|
+
}
|
|
803
|
+
return { length: 0, start: () => 0, end: () => 0 };
|
|
804
|
+
},
|
|
805
|
+
configurable: true,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
sb._emit("updateend");
|
|
809
|
+
await expect(promise).resolves.toEqual([]);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("rejects with non-InvalidStateError thrown from buffered on updateend", async () => {
|
|
813
|
+
const iface = createMainSourceBufferInterface();
|
|
814
|
+
const data = new Uint8Array([1]);
|
|
815
|
+
const promise = iface.appendBuffer(data, { codec: "video/mp4" });
|
|
816
|
+
|
|
817
|
+
const e = new Error("some buffered error");
|
|
818
|
+
e.name = "SomeOtherError";
|
|
819
|
+
Object.defineProperty(sb, "buffered", {
|
|
820
|
+
get() {
|
|
821
|
+
throw e;
|
|
822
|
+
},
|
|
823
|
+
configurable: true,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
sb._emit("updateend");
|
|
827
|
+
await expect(promise).rejects.toMatchObject(e);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("logs error when error event fires with no current operations", () => {
|
|
831
|
+
createMainSourceBufferInterface();
|
|
832
|
+
// No pending operation – just fire error; should not throw
|
|
833
|
+
expect(() => sb._emit("error", new Error("orphan error"))).not.toThrow();
|
|
834
|
+
// TODO: check logs?
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("does not call appendBuffer again if sourceBuffer.updating is true", () => {
|
|
838
|
+
const mockAppendBuffer = vi.spyOn(MockSourceBuffer.prototype, "appendBuffer");
|
|
839
|
+
const iface = createMainSourceBufferInterface();
|
|
840
|
+
sb.updating = true;
|
|
841
|
+
const data = new Uint8Array([1]);
|
|
842
|
+
iface.appendBuffer(data, { codec: "video/mp4" }).catch(noop);
|
|
843
|
+
// Because updating=true _performNextOperation bails out
|
|
844
|
+
expect(mockAppendBuffer).not.toHaveBeenCalled();
|
|
845
|
+
});
|
|
846
|
+
});
|