@videojs/html 10.0.0-beta.10 → 10.0.0-beta.11
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/cdn/audio-minimal.dev.js +4 -3
- package/cdn/audio-minimal.dev.js.map +1 -1
- package/cdn/audio-minimal.js +1 -1
- package/cdn/audio-minimal.js.map +1 -1
- package/cdn/audio.dev.js +4 -3
- package/cdn/audio.dev.js.map +1 -1
- package/cdn/audio.js +1 -1
- package/cdn/audio.js.map +1 -1
- package/cdn/background.dev.js +3 -2
- package/cdn/background.dev.js.map +1 -1
- package/cdn/background.js +1 -1
- package/cdn/background.js.map +1 -1
- package/cdn/{context-DTY0nOpS.js → context-Be8C5kVd.js} +70 -2
- package/cdn/context-Be8C5kVd.js.map +1 -0
- package/cdn/context-CUBywtsB.js +14 -0
- package/cdn/context-CUBywtsB.js.map +1 -0
- package/cdn/{create-player-Cwxvswyv.js → create-player-AcfnN3li.js} +3 -3
- package/cdn/{create-player-Cwxvswyv.js.map → create-player-AcfnN3li.js.map} +1 -1
- package/cdn/create-player-s_qISCpw.js +7 -0
- package/cdn/{create-player-BTIU8EwT.js.map → create-player-s_qISCpw.js.map} +1 -1
- package/cdn/{proxy-2oO2ph3m.js → custom-media-element-DqevSVgS.js} +6 -6
- package/cdn/custom-media-element-DqevSVgS.js.map +1 -0
- package/cdn/{proxy-dR7IDk37.js → custom-media-element-moFa3UZp.js} +2 -48
- package/cdn/custom-media-element-moFa3UZp.js.map +1 -0
- package/cdn/delegate-CzAcT1xm.js +44 -0
- package/cdn/delegate-CzAcT1xm.js.map +1 -0
- package/cdn/delegate-Uc-6tQDR.js +2 -0
- package/cdn/delegate-Uc-6tQDR.js.map +1 -0
- package/cdn/{listen-DX5vU4s4.js → listen-4jqsRSKo.js} +1 -1
- package/cdn/{listen-DX5vU4s4.js.map → listen-4jqsRSKo.js.map} +1 -1
- package/cdn/{listen-BXAYCbZA.js → listen-YSH3Jfyk.js} +1 -1
- package/cdn/{listen-BXAYCbZA.js.map → listen-YSH3Jfyk.js.map} +1 -1
- package/cdn/media/dash-video.dev.js +4 -2
- package/cdn/media/dash-video.dev.js.map +1 -1
- package/cdn/media/dash-video.js +2 -2
- package/cdn/media/dash-video.js.map +1 -1
- package/cdn/media/hls-video.dev.js +5 -3
- package/cdn/media/hls-video.dev.js.map +1 -1
- package/cdn/media/hls-video.js +2 -2
- package/cdn/media/hls-video.js.map +1 -1
- package/cdn/media/simple-hls-video.dev.js +684 -546
- package/cdn/media/simple-hls-video.dev.js.map +1 -1
- package/cdn/media/simple-hls-video.js +1 -1
- package/cdn/media/simple-hls-video.js.map +1 -1
- package/cdn/{media-attach-mixin-tFNcHnvo.js → media-attach-mixin-D5_nfJpa.js} +2 -2
- package/cdn/{media-attach-mixin-tFNcHnvo.js.map → media-attach-mixin-D5_nfJpa.js.map} +1 -1
- package/cdn/{media-attach-mixin-ChyNp2eK.js → media-attach-mixin-U_KQB_9O.js} +2 -2
- package/cdn/{media-attach-mixin-ChyNp2eK.js.map → media-attach-mixin-U_KQB_9O.js.map} +1 -1
- package/cdn/{player-BHhLXO-R.js → player-C46h14iP.js} +2 -2
- package/cdn/{player-BHhLXO-R.js.map → player-C46h14iP.js.map} +1 -1
- package/cdn/{player-DEfj0RU6.js → player-CvrOeLpy.js} +2 -2
- package/cdn/{player-DEfj0RU6.js.map → player-CvrOeLpy.js.map} +1 -1
- package/cdn/{poster-Dd0F1rRd.js → poster-Olv5zDI_.js} +4 -4
- package/cdn/{poster-Dd0F1rRd.js.map → poster-Olv5zDI_.js.map} +1 -1
- package/cdn/{poster-DwQ3RAch.js → poster-odJ4iwIv.js} +2 -2
- package/cdn/{poster-DwQ3RAch.js.map → poster-odJ4iwIv.js.map} +1 -1
- package/cdn/video-minimal.dev.js +4 -3
- package/cdn/video-minimal.dev.js.map +1 -1
- package/cdn/video-minimal.js +1 -1
- package/cdn/video-minimal.js.map +1 -1
- package/cdn/video.dev.js +4 -4
- package/cdn/video.js +1 -1
- package/cdn/{volume-slider-DgJ0rAfC.js → volume-slider-D7BOdSDF.js} +3 -3
- package/cdn/{volume-slider-DgJ0rAfC.js.map → volume-slider-D7BOdSDF.js.map} +1 -1
- package/cdn/{volume-slider-Pd0AMTCH.js → volume-slider-DPeFF5tt.js} +2 -2
- package/cdn/{volume-slider-Pd0AMTCH.js.map → volume-slider-DPeFF5tt.js.map} +1 -1
- package/dist/default/index.js +2 -1
- package/dist/default/ui/alert-dialog/alert-dialog-element.js +1 -1
- package/dist/default/ui/buffering-indicator/buffering-indicator-element.js +1 -1
- package/dist/default/ui/captions-button/captions-button-element.js +1 -1
- package/dist/default/ui/controls/controls-element.js +1 -1
- package/dist/default/ui/fullscreen-button/fullscreen-button-element.js +1 -1
- package/dist/default/ui/mute-button/mute-button-element.js +1 -1
- package/dist/default/ui/pip-button/pip-button-element.js +1 -1
- package/dist/default/ui/play-button/play-button-element.js +1 -1
- package/dist/default/ui/playback-rate-button/playback-rate-button-element.js +1 -1
- package/dist/default/ui/popover/popover-element.js +1 -1
- package/dist/default/ui/poster/poster-element.js +1 -1
- package/dist/default/ui/seek-button/seek-button-element.js +1 -1
- package/dist/default/ui/slider/slider-element.js +1 -1
- package/dist/default/ui/thumbnail/thumbnail-element.js +1 -1
- package/dist/default/ui/time/time-element.js +1 -1
- package/dist/default/ui/time-slider/time-slider-element.js +1 -1
- package/dist/default/ui/tooltip/tooltip-element.js +1 -1
- package/dist/default/ui/tooltip/tooltip-group-element.js +1 -1
- package/dist/default/ui/volume-slider/volume-slider-element.js +1 -1
- package/dist/dev/index.d.ts +2 -1
- package/dist/dev/index.js +2 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-description-element.d.ts +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-element.js +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-title-element.d.ts +1 -1
- package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js +1 -1
- package/dist/dev/ui/captions-button/captions-button-element.d.ts +1 -1
- package/dist/dev/ui/captions-button/captions-button-element.js +1 -1
- package/dist/dev/ui/context-part-element.d.ts +1 -1
- package/dist/dev/ui/controls/controls-element.js +1 -1
- package/dist/dev/ui/controls/controls-group-element.d.ts +1 -1
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.d.ts +1 -1
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js +1 -1
- package/dist/dev/ui/media-button-element.d.ts +1 -1
- package/dist/dev/ui/media-ui-element.d.ts +1 -1
- package/dist/dev/ui/mute-button/mute-button-element.d.ts +1 -1
- package/dist/dev/ui/mute-button/mute-button-element.js +1 -1
- package/dist/dev/ui/pip-button/pip-button-element.d.ts +1 -1
- package/dist/dev/ui/pip-button/pip-button-element.js +1 -1
- package/dist/dev/ui/play-button/play-button-element.d.ts +1 -1
- package/dist/dev/ui/play-button/play-button-element.js +1 -1
- package/dist/dev/ui/playback-rate-button/playback-rate-button-element.d.ts +1 -1
- package/dist/dev/ui/playback-rate-button/playback-rate-button-element.js +1 -1
- package/dist/dev/ui/popover/popover-element.d.ts +1 -1
- package/dist/dev/ui/popover/popover-element.js +1 -1
- package/dist/dev/ui/poster/poster-element.d.ts +1 -1
- package/dist/dev/ui/poster/poster-element.js +1 -1
- package/dist/dev/ui/seek-button/seek-button-element.d.ts +1 -1
- package/dist/dev/ui/seek-button/seek-button-element.js +1 -1
- package/dist/dev/ui/slider/context.d.ts +1 -1
- package/dist/dev/ui/slider/slider-buffer-element.d.ts +1 -1
- package/dist/dev/ui/slider/slider-element.d.ts +1 -1
- package/dist/dev/ui/slider/slider-element.js +1 -1
- package/dist/dev/ui/slider/slider-fill-element.d.ts +1 -1
- package/dist/dev/ui/slider/slider-track-element.d.ts +1 -1
- package/dist/dev/ui/thumbnail/thumbnail-element.d.ts +1 -1
- package/dist/dev/ui/thumbnail/thumbnail-element.js +1 -1
- package/dist/dev/ui/time/time-element.d.ts +1 -1
- package/dist/dev/ui/time/time-element.js +1 -1
- package/dist/dev/ui/time-slider/time-slider-element.d.ts +1 -1
- package/dist/dev/ui/time-slider/time-slider-element.js +1 -1
- package/dist/dev/ui/tooltip/tooltip-element.d.ts +1 -1
- package/dist/dev/ui/tooltip/tooltip-element.js +1 -1
- package/dist/dev/ui/tooltip/tooltip-group-element.js +1 -1
- package/dist/dev/ui/volume-slider/volume-slider-element.d.ts +1 -1
- package/dist/dev/ui/volume-slider/volume-slider-element.js +1 -1
- package/package.json +10 -10
- package/cdn/context-C_e06fGU.js +0 -13
- package/cdn/context-C_e06fGU.js.map +0 -1
- package/cdn/context-DTY0nOpS.js.map +0 -1
- package/cdn/create-player-BTIU8EwT.js +0 -7
- package/cdn/proxy-2oO2ph3m.js.map +0 -1
- package/cdn/proxy-6KS6wy69.js +0 -2
- package/cdn/proxy-6KS6wy69.js.map +0 -1
- package/cdn/proxy-XzDf9gyk.js +0 -66
- package/cdn/proxy-XzDf9gyk.js.map +0 -1
- package/cdn/proxy-dR7IDk37.js.map +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { n as isNil } from "../predicate-BG-dj_kF.js";
|
|
2
|
-
import
|
|
3
|
-
import { t as
|
|
4
|
-
import {
|
|
2
|
+
import "../context-Be8C5kVd.js";
|
|
3
|
+
import { t as listen } from "../listen-YSH3Jfyk.js";
|
|
4
|
+
import { t as DelegateMixin } from "../delegate-CzAcT1xm.js";
|
|
5
|
+
import { t as MediaAttachMixin } from "../media-attach-mixin-U_KQB_9O.js";
|
|
6
|
+
import { t as CustomMediaMixin } from "../custom-media-element-moFa3UZp.js";
|
|
5
7
|
|
|
6
|
-
//#region ../spf/dist/
|
|
8
|
+
//#region ../spf/dist/dev/core/state/create-state.js
|
|
7
9
|
/**
|
|
8
10
|
* Reactive state container with selectors, custom equality, and batched updates.
|
|
9
11
|
*
|
|
@@ -61,11 +63,11 @@ var StateContainer = class {
|
|
|
61
63
|
}
|
|
62
64
|
subscribe(selectorOrListener, maybeListener, options) {
|
|
63
65
|
if (maybeListener === void 0) {
|
|
64
|
-
const listener
|
|
65
|
-
this.#listeners.add(listener
|
|
66
|
-
listener
|
|
66
|
+
const listener = selectorOrListener;
|
|
67
|
+
this.#listeners.add(listener);
|
|
68
|
+
listener(this.current);
|
|
67
69
|
return () => {
|
|
68
|
-
this.#listeners.delete(listener
|
|
70
|
+
this.#listeners.delete(listener);
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
const selector = selectorOrListener;
|
|
@@ -142,6 +144,432 @@ var StateContainer = class {
|
|
|
142
144
|
function createState(initial, config) {
|
|
143
145
|
return new StateContainer(initial, config);
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region ../spf/dist/dev/core/abr/ewma.js
|
|
150
|
+
/**
|
|
151
|
+
* Exponentially Weighted Moving Average (EWMA)
|
|
152
|
+
*
|
|
153
|
+
* Pure functional implementation of EWMA calculations.
|
|
154
|
+
* Based on Shaka Player's EWMA algorithm.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Calculate alpha (decay factor) from half-life.
|
|
158
|
+
*
|
|
159
|
+
* Alpha determines how quickly old data "expires":
|
|
160
|
+
* - alpha close to 1 = slow decay (long memory)
|
|
161
|
+
* - alpha close to 0 = fast decay (short memory)
|
|
162
|
+
*
|
|
163
|
+
* @param halfLife - The quantity of prior samples (by weight) that make up
|
|
164
|
+
* half of the new estimate. Must be positive.
|
|
165
|
+
* @returns Alpha value between 0 and 1
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life
|
|
169
|
+
*/
|
|
170
|
+
function calculateAlpha(halfLife) {
|
|
171
|
+
return Math.exp(Math.log(.5) / halfLife);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Calculate exponentially weighted moving average.
|
|
175
|
+
*
|
|
176
|
+
* Updates an estimate by blending a new value with the previous estimate,
|
|
177
|
+
* weighted by the sample duration. Longer samples have more influence.
|
|
178
|
+
*
|
|
179
|
+
* @param prevEstimate - Previous EWMA estimate
|
|
180
|
+
* @param value - New sample value to incorporate
|
|
181
|
+
* @param weight - Sample weight (typically duration in seconds)
|
|
182
|
+
* @param halfLife - Half-life for decay (typically 2-5 seconds)
|
|
183
|
+
* @returns Updated EWMA estimate
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* let estimate = 0;
|
|
187
|
+
* estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample
|
|
188
|
+
* estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample
|
|
189
|
+
*/
|
|
190
|
+
function calculateEwma(prevEstimate, value, weight, halfLife) {
|
|
191
|
+
const adjAlpha = calculateAlpha(halfLife) ** weight;
|
|
192
|
+
return value * (1 - adjAlpha) + adjAlpha * prevEstimate;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Apply zero-factor correction to EWMA estimate.
|
|
196
|
+
*
|
|
197
|
+
* The zero-factor correction compensates for bias when starting from zero.
|
|
198
|
+
* Without this correction, early estimates would be artificially low.
|
|
199
|
+
*
|
|
200
|
+
* As totalWeight increases, the correction factor approaches 1, meaning
|
|
201
|
+
* the estimate becomes more reliable and needs less correction.
|
|
202
|
+
*
|
|
203
|
+
* @param estimate - Raw EWMA estimate (uncorrected)
|
|
204
|
+
* @param totalWeight - Accumulated weight from all samples
|
|
205
|
+
* @param halfLife - Half-life used in EWMA calculation
|
|
206
|
+
* @returns Corrected estimate, or 0 if totalWeight is 0
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* const raw = calculateEwma(0, 1_000_000, 1, 2);
|
|
210
|
+
* const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000
|
|
211
|
+
*/
|
|
212
|
+
function applyZeroFactor(estimate, totalWeight, halfLife) {
|
|
213
|
+
if (totalWeight === 0) return 0;
|
|
214
|
+
return estimate / (1 - calculateAlpha(halfLife) ** totalWeight);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region ../spf/dist/dev/core/abr/bandwidth-estimator.js
|
|
219
|
+
/**
|
|
220
|
+
* Dual EWMA Bandwidth Estimator
|
|
221
|
+
*
|
|
222
|
+
* Estimates available bandwidth using two EWMA calculations with different
|
|
223
|
+
* half-lives, taking the minimum of both. This approach (from Shaka Player):
|
|
224
|
+
*
|
|
225
|
+
* - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
|
|
226
|
+
* - **Slow EWMA** (5s half-life): Provides stability during fluctuations
|
|
227
|
+
* - **min(fast, slow)**: Adapts down quickly, up slowly
|
|
228
|
+
*
|
|
229
|
+
* This naturally provides asymmetric behavior needed for good QoE:
|
|
230
|
+
* avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
|
|
231
|
+
*/
|
|
232
|
+
/**
|
|
233
|
+
* Default bandwidth estimator configuration.
|
|
234
|
+
*
|
|
235
|
+
* Values match Shaka Player defaults based on experimentation.
|
|
236
|
+
*/
|
|
237
|
+
const DEFAULT_BANDWIDTH_CONFIG = {
|
|
238
|
+
fastHalfLife: 2,
|
|
239
|
+
slowHalfLife: 5,
|
|
240
|
+
minTotalBytes: 128e3,
|
|
241
|
+
minBytes: 16e3,
|
|
242
|
+
minDuration: 5
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Add a bandwidth sample from a segment download.
|
|
246
|
+
*
|
|
247
|
+
* Samples are filtered based on:
|
|
248
|
+
* - Minimum bytes (filters TTFB-dominated small segments)
|
|
249
|
+
* - Minimum duration (filters cached responses)
|
|
250
|
+
*
|
|
251
|
+
* Valid samples update both fast and slow EWMA estimates.
|
|
252
|
+
*
|
|
253
|
+
* @param state - Current estimator state
|
|
254
|
+
* @param durationMs - Download duration in milliseconds
|
|
255
|
+
* @param numBytes - Number of bytes downloaded
|
|
256
|
+
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
257
|
+
* @returns New estimator state with sample incorporated (or unchanged if filtered)
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* let state = { fastEstimate: 0, fastTotalWeight: 0, ... };
|
|
261
|
+
* // Sample: 1MB in 1 second
|
|
262
|
+
* state = sampleBandwidth(state, 1000, 1_000_000);
|
|
263
|
+
*/
|
|
264
|
+
function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
265
|
+
const updatedBytesSampled = state.bytesSampled + numBytes;
|
|
266
|
+
if (numBytes < config.minBytes) return {
|
|
267
|
+
...state,
|
|
268
|
+
bytesSampled: updatedBytesSampled
|
|
269
|
+
};
|
|
270
|
+
if (durationMs < config.minDuration) return {
|
|
271
|
+
...state,
|
|
272
|
+
bytesSampled: updatedBytesSampled
|
|
273
|
+
};
|
|
274
|
+
const bandwidth = 8e3 * numBytes / durationMs;
|
|
275
|
+
const weight = durationMs / 1e3;
|
|
276
|
+
return {
|
|
277
|
+
fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),
|
|
278
|
+
fastTotalWeight: state.fastTotalWeight + weight,
|
|
279
|
+
slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),
|
|
280
|
+
slowTotalWeight: state.slowTotalWeight + weight,
|
|
281
|
+
bytesSampled: updatedBytesSampled
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get the current bandwidth estimate.
|
|
286
|
+
*
|
|
287
|
+
* Returns the **minimum** of the fast and slow EWMA estimates.
|
|
288
|
+
* This provides the key asymmetric behavior:
|
|
289
|
+
* - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
|
|
290
|
+
* - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
|
|
291
|
+
*
|
|
292
|
+
* Uses default estimate until enough data has been sampled.
|
|
293
|
+
*
|
|
294
|
+
* @param state - Current estimator state
|
|
295
|
+
* @param defaultEstimate - Fallback estimate before sufficient samples (bps)
|
|
296
|
+
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
297
|
+
* @returns Bandwidth estimate in bits per second
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
|
|
301
|
+
*/
|
|
302
|
+
function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
303
|
+
if (state.bytesSampled < config.minTotalBytes) return defaultEstimate;
|
|
304
|
+
const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
|
|
305
|
+
const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
|
|
306
|
+
return Math.min(fastEstimate, slowEstimate);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region ../spf/dist/dev/core/buffer/forward-buffer.js
|
|
311
|
+
/**
|
|
312
|
+
* Default forward buffer configuration.
|
|
313
|
+
*/
|
|
314
|
+
const DEFAULT_FORWARD_BUFFER_CONFIG = { bufferDuration: 30 };
|
|
315
|
+
/**
|
|
316
|
+
* Get segments that need to be loaded for forward buffer.
|
|
317
|
+
*
|
|
318
|
+
* Determines which segments to load to maintain target buffer duration.
|
|
319
|
+
* Handles discontiguous buffering (gaps after seeks).
|
|
320
|
+
*
|
|
321
|
+
* Algorithm:
|
|
322
|
+
* 1. Calculate target time: currentTime + bufferDuration
|
|
323
|
+
* 2. Find all segments in range [currentTime, targetTime)
|
|
324
|
+
* 3. Filter out segments already buffered at that time position
|
|
325
|
+
* 4. Return segments to load (fills gaps + extends to target)
|
|
326
|
+
*
|
|
327
|
+
* @param segments - All available segments from playlist
|
|
328
|
+
* @param bufferedSegments - Segments already buffered (ordered by startTime)
|
|
329
|
+
* @param currentTime - Current playback position in seconds
|
|
330
|
+
* @param config - Optional forward buffer configuration
|
|
331
|
+
* @returns Array of segments to load (empty if buffer is sufficient)
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* // After seek: buffered [0-12, 18-30], playing at 7s
|
|
335
|
+
* const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });
|
|
336
|
+
* // Returns [seg-12, seg-30] (fills gap, extends to target 31s)
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* Calculate the start time from which to flush forward buffer content.
|
|
340
|
+
*
|
|
341
|
+
* Content that starts at or beyond `currentTime + bufferDuration` is no
|
|
342
|
+
* longer needed for the current playback position and should be removed
|
|
343
|
+
* from the SourceBuffer. This prevents unbounded accumulation of scattered
|
|
344
|
+
* SourceBuffer content after seeks, which can cause QuotaExceededError on
|
|
345
|
+
* long-form content.
|
|
346
|
+
*
|
|
347
|
+
* Returns `Infinity` when nothing needs flushing (no buffered segments
|
|
348
|
+
* exist beyond the threshold).
|
|
349
|
+
*
|
|
350
|
+
* @param bufferedSegments - Segments currently tracked in the buffer model
|
|
351
|
+
* @param currentTime - Current playback position in seconds
|
|
352
|
+
* @param config - Optional forward buffer configuration
|
|
353
|
+
* @returns Start time to flush from (flush range: [flushStart, Infinity)),
|
|
354
|
+
* or Infinity if no flush is needed
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30
|
|
358
|
+
* const flushStart = calculateForwardFlushPoint(segments, 0);
|
|
359
|
+
* // Returns 30 — flush [30, Infinity), keep [0, 30)
|
|
360
|
+
*/
|
|
361
|
+
function calculateForwardFlushPoint(bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
|
|
362
|
+
if (bufferedSegments.length === 0) return Infinity;
|
|
363
|
+
const threshold = currentTime + config.bufferDuration;
|
|
364
|
+
const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);
|
|
365
|
+
if (beyond.length === 0) return Infinity;
|
|
366
|
+
return Math.min(...beyond.map((seg) => seg.startTime));
|
|
367
|
+
}
|
|
368
|
+
function getSegmentsToLoad(segments, bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
|
|
369
|
+
if (segments.length === 0) return [];
|
|
370
|
+
const targetTime = currentTime + config.bufferDuration;
|
|
371
|
+
const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));
|
|
372
|
+
return segments.filter((seg) => {
|
|
373
|
+
const segmentEnd = seg.startTime + seg.duration;
|
|
374
|
+
const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;
|
|
375
|
+
const isNotBuffered = !bufferedStartTimes.has(seg.startTime);
|
|
376
|
+
return isInRange && isNotBuffered;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region ../spf/dist/dev/core/types/index.js
|
|
382
|
+
function isResolvedTrack(track) {
|
|
383
|
+
return "segments" in track;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Check if a presentation has duration (at least one track resolved).
|
|
387
|
+
* Narrows type to include required duration.
|
|
388
|
+
*/
|
|
389
|
+
function hasPresentationDuration(presentation) {
|
|
390
|
+
return presentation.duration !== void 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region ../spf/dist/dev/dom/network/chunked-stream-iterable.js
|
|
395
|
+
const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17;
|
|
396
|
+
/**
|
|
397
|
+
* Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an
|
|
398
|
+
* `AsyncIterable<Uint8Array>` that yields chunks no smaller than
|
|
399
|
+
* `minChunkSize` bytes. Smaller network chunks are accumulated and yielded
|
|
400
|
+
* together once the threshold is met. Any remainder is flushed on stream end.
|
|
401
|
+
*
|
|
402
|
+
* Errors from the underlying stream propagate naturally — the reader lock is
|
|
403
|
+
* always released via `finally`.
|
|
404
|
+
*/
|
|
405
|
+
var ChunkedStreamIterable = class {
|
|
406
|
+
minChunkSize;
|
|
407
|
+
#readableStream;
|
|
408
|
+
constructor(readableStream, { minChunkSize = DEFAULT_MIN_CHUNK_SIZE } = {}) {
|
|
409
|
+
this.#readableStream = readableStream;
|
|
410
|
+
this.minChunkSize = minChunkSize;
|
|
411
|
+
}
|
|
412
|
+
async *[Symbol.asyncIterator]() {
|
|
413
|
+
let pending;
|
|
414
|
+
const reader = this.#readableStream.getReader();
|
|
415
|
+
try {
|
|
416
|
+
while (true) {
|
|
417
|
+
const { done, value } = await reader.read();
|
|
418
|
+
if (done) {
|
|
419
|
+
if (pending) yield pending;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
pending = pending ? concat(pending, value) : value;
|
|
423
|
+
if (pending.length >= this.minChunkSize) {
|
|
424
|
+
yield pending;
|
|
425
|
+
pending = void 0;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} finally {
|
|
429
|
+
reader.releaseLock();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
function concat(a, b) {
|
|
434
|
+
const result = new Uint8Array(a.length + b.length);
|
|
435
|
+
result.set(a);
|
|
436
|
+
result.set(b, a.length);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region ../spf/dist/dev/dom/network/fetch.js
|
|
442
|
+
/**
|
|
443
|
+
* Fetch resolvable from AddressableObject.
|
|
444
|
+
*
|
|
445
|
+
* Handles byte range requests if byteRange is present.
|
|
446
|
+
* Returns native fetch Response for composability (can extract text, stream, etc.).
|
|
447
|
+
*
|
|
448
|
+
* @param addressable - Resource to fetch (url + optional byteRange)
|
|
449
|
+
* @returns Promise resolving to Response
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
|
|
453
|
+
* const text = await getResponseText(response);
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* // With byte range
|
|
457
|
+
* const response = await fetchResolvable({
|
|
458
|
+
* url: 'https://example.com/file.mp4',
|
|
459
|
+
* byteRange: { start: 1000, end: 1999 }
|
|
460
|
+
* });
|
|
461
|
+
*/
|
|
462
|
+
async function fetchResolvable(addressable, options) {
|
|
463
|
+
const headers = new Headers(options?.headers);
|
|
464
|
+
if (addressable.byteRange) {
|
|
465
|
+
const { start, end } = addressable.byteRange;
|
|
466
|
+
headers.set("Range", `bytes=${start}-${end}`);
|
|
467
|
+
}
|
|
468
|
+
const request = new Request(addressable.url, {
|
|
469
|
+
method: "GET",
|
|
470
|
+
headers,
|
|
471
|
+
...options
|
|
472
|
+
});
|
|
473
|
+
return fetch(request);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Extract text from Response.
|
|
477
|
+
*
|
|
478
|
+
* Accepts minimal Response-like object (just needs text() method).
|
|
479
|
+
* Returns promise from response.text().
|
|
480
|
+
*
|
|
481
|
+
* @param response - Response-like object with text() method
|
|
482
|
+
* @returns Promise resolving to text content
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* const response = await fetchResolvable(addressable);
|
|
486
|
+
* const text = await getResponseText(response);
|
|
487
|
+
*/
|
|
488
|
+
function getResponseText(response) {
|
|
489
|
+
return response.text();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region ../spf/dist/dev/core/reactive/combine-latest.js
|
|
494
|
+
/**
|
|
495
|
+
* Combines multiple Observable sources into a single Observable.
|
|
496
|
+
*
|
|
497
|
+
* Emits an array of latest values whenever any source emits.
|
|
498
|
+
* Only emits after all sources have emitted at least once.
|
|
499
|
+
*
|
|
500
|
+
* Supports selector-based subscriptions (fires only when the selected value
|
|
501
|
+
* changes, per the optional equalityFn) mirroring the createState API.
|
|
502
|
+
*
|
|
503
|
+
* @param sources - Array of Observable sources
|
|
504
|
+
* @returns Combined Observable
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* const state = createState({ count: 0 });
|
|
509
|
+
* const events = createEventStream<Action>();
|
|
510
|
+
*
|
|
511
|
+
* combineLatest([state, events]).subscribe(([state, event]) => {
|
|
512
|
+
* if (event.type === 'PLAY' && state.count > 0) {
|
|
513
|
+
* // React to event + state condition
|
|
514
|
+
* }
|
|
515
|
+
* });
|
|
516
|
+
* ```
|
|
517
|
+
*
|
|
518
|
+
* @example Selector subscription
|
|
519
|
+
* ```ts
|
|
520
|
+
* combineLatest([state, owners]).subscribe(
|
|
521
|
+
* ([s, o]) => deriveKey(s, o),
|
|
522
|
+
* (key) => { ... },
|
|
523
|
+
* { equalityFn: keyEq }
|
|
524
|
+
* );
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
function combineLatest(sources) {
|
|
528
|
+
const subscribeToSources = (listener) => {
|
|
529
|
+
const latest = new Array(sources.length);
|
|
530
|
+
const hasValue = new Array(sources.length).fill(false);
|
|
531
|
+
const unsubscribers = [];
|
|
532
|
+
for (let i = 0; i < sources.length; i++) {
|
|
533
|
+
const unsubscribe = sources[i].subscribe((value) => {
|
|
534
|
+
latest[i] = value;
|
|
535
|
+
hasValue[i] = true;
|
|
536
|
+
if (hasValue.every((has) => has)) listener([...latest]);
|
|
537
|
+
});
|
|
538
|
+
unsubscribers.push(unsubscribe);
|
|
539
|
+
}
|
|
540
|
+
return () => {
|
|
541
|
+
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
return { subscribe(listenerOrSelector, maybeListener, options) {
|
|
545
|
+
if (maybeListener === void 0) return subscribeToSources(listenerOrSelector);
|
|
546
|
+
const selector = listenerOrSelector;
|
|
547
|
+
const listener = maybeListener;
|
|
548
|
+
const equalityFn = options?.equalityFn ?? Object.is;
|
|
549
|
+
let prevSelected;
|
|
550
|
+
let initialized = false;
|
|
551
|
+
return subscribeToSources((values) => {
|
|
552
|
+
const nextSelected = selector(values);
|
|
553
|
+
if (!initialized || !equalityFn(prevSelected, nextSelected)) {
|
|
554
|
+
prevSelected = nextSelected;
|
|
555
|
+
initialized = true;
|
|
556
|
+
listener(nextSelected);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
} };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region ../spf/dist/dev/core/hls/resolve-url.js
|
|
564
|
+
/**
|
|
565
|
+
* Resolve a potentially relative URL against a base URL using native URL API.
|
|
566
|
+
*/
|
|
567
|
+
function resolveUrl(url, baseUrl) {
|
|
568
|
+
return new URL(url, baseUrl).href;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region ../spf/dist/dev/core/hls/parse-attributes.js
|
|
145
573
|
/**
|
|
146
574
|
* Parse HLS attribute list from a tag line.
|
|
147
575
|
* Handles both quoted and unquoted values.
|
|
@@ -271,12 +699,9 @@ function matchTag(line, tag) {
|
|
|
271
699
|
if (!line.startsWith(prefix)) return null;
|
|
272
700
|
return createAttributeList(line.slice(prefix.length));
|
|
273
701
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
function resolveUrl(url, baseUrl) {
|
|
278
|
-
return new URL(url, baseUrl).href;
|
|
279
|
-
}
|
|
702
|
+
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region ../spf/dist/dev/core/hls/parse-media-playlist.js
|
|
280
705
|
/**
|
|
281
706
|
* Parse HLS media playlist and resolve track with segments.
|
|
282
707
|
*
|
|
@@ -352,6 +777,9 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
352
777
|
initialization
|
|
353
778
|
};
|
|
354
779
|
}
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region ../spf/dist/dev/core/utils/generate-id.js
|
|
355
783
|
/**
|
|
356
784
|
* Generate unique ID for HAM objects.
|
|
357
785
|
*
|
|
@@ -368,6 +796,9 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
368
796
|
function generateId() {
|
|
369
797
|
return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
370
798
|
}
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region ../spf/dist/dev/core/hls/parse-multivariant.js
|
|
371
802
|
/**
|
|
372
803
|
* Parse HLS multivariant playlist into a Presentation.
|
|
373
804
|
*
|
|
@@ -460,262 +891,123 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
460
891
|
bandwidth: stream.bandwidth,
|
|
461
892
|
mimeType: "video/mp4",
|
|
462
893
|
codecs: []
|
|
463
|
-
};
|
|
464
|
-
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
465
|
-
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
466
|
-
if (codecs?.video) track.codecs = [codecs.video];
|
|
467
|
-
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
468
|
-
if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
|
|
469
|
-
return track;
|
|
470
|
-
});
|
|
471
|
-
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
472
|
-
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
473
|
-
return {
|
|
474
|
-
type: "audio",
|
|
475
|
-
id: generateId(),
|
|
476
|
-
url: stream.uri,
|
|
477
|
-
bandwidth: stream.bandwidth,
|
|
478
|
-
mimeType: "audio/mp4",
|
|
479
|
-
codecs: codecs?.audio ? [codecs.audio] : [],
|
|
480
|
-
groupId: stream.audioGroupId || "default",
|
|
481
|
-
name: "Default",
|
|
482
|
-
sampleRate: 48e3,
|
|
483
|
-
channels: 2
|
|
484
|
-
};
|
|
485
|
-
});
|
|
486
|
-
const audioTracks = [...audioRenditions.map((rendition) => {
|
|
487
|
-
let audioCodecs;
|
|
488
|
-
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
489
|
-
const codecs = parseCodecs(stream.codecs);
|
|
490
|
-
if (codecs.audio) {
|
|
491
|
-
audioCodecs = [codecs.audio];
|
|
492
|
-
break;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const track = {
|
|
496
|
-
type: "audio",
|
|
497
|
-
id: generateId(),
|
|
498
|
-
url: rendition.uri ?? "",
|
|
499
|
-
groupId: rendition.groupId,
|
|
500
|
-
name: rendition.name,
|
|
501
|
-
mimeType: "audio/mp4",
|
|
502
|
-
bandwidth: 0,
|
|
503
|
-
sampleRate: 48e3,
|
|
504
|
-
channels: 2,
|
|
505
|
-
codecs: []
|
|
506
|
-
};
|
|
507
|
-
if (rendition.language) track.language = rendition.language;
|
|
508
|
-
if (audioCodecs) track.codecs = audioCodecs;
|
|
509
|
-
if (rendition.default) track.default = rendition.default;
|
|
510
|
-
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
511
|
-
return track;
|
|
512
|
-
}), ...audioOnlyTracks];
|
|
513
|
-
const textTracks = subtitleRenditions.map((rendition) => {
|
|
514
|
-
const track = {
|
|
515
|
-
type: "text",
|
|
516
|
-
id: generateId(),
|
|
517
|
-
url: rendition.uri,
|
|
518
|
-
groupId: rendition.groupId,
|
|
519
|
-
label: rendition.name,
|
|
520
|
-
kind: "subtitles",
|
|
521
|
-
mimeType: "text/vtt",
|
|
522
|
-
bandwidth: 0
|
|
523
|
-
};
|
|
524
|
-
if (rendition.language) track.language = rendition.language;
|
|
525
|
-
if (rendition.default && rendition.autoselect) track.default = true;
|
|
526
|
-
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
527
|
-
if (rendition.forced) track.forced = rendition.forced;
|
|
528
|
-
return track;
|
|
529
|
-
});
|
|
530
|
-
const selectionSets = [];
|
|
531
|
-
if (videoTracks.length > 0) {
|
|
532
|
-
const videoSwitchingSet = {
|
|
533
|
-
id: generateId(),
|
|
534
|
-
type: "video",
|
|
535
|
-
tracks: videoTracks
|
|
536
|
-
};
|
|
537
|
-
const videoSelectionSet = {
|
|
538
|
-
id: generateId(),
|
|
539
|
-
type: "video",
|
|
540
|
-
switchingSets: [videoSwitchingSet]
|
|
541
|
-
};
|
|
542
|
-
selectionSets.push(videoSelectionSet);
|
|
543
|
-
}
|
|
544
|
-
if (audioTracks.length > 0) {
|
|
545
|
-
const audioSwitchingSet = {
|
|
546
|
-
id: generateId(),
|
|
547
|
-
type: "audio",
|
|
548
|
-
tracks: audioTracks
|
|
549
|
-
};
|
|
550
|
-
const audioSelectionSet = {
|
|
551
|
-
id: generateId(),
|
|
552
|
-
type: "audio",
|
|
553
|
-
switchingSets: [audioSwitchingSet]
|
|
554
|
-
};
|
|
555
|
-
selectionSets.push(audioSelectionSet);
|
|
556
|
-
}
|
|
557
|
-
if (textTracks.length > 0) {
|
|
558
|
-
const textSwitchingSet = {
|
|
559
|
-
id: generateId(),
|
|
560
|
-
type: "text",
|
|
561
|
-
tracks: textTracks
|
|
562
|
-
};
|
|
563
|
-
const textSelectionSet = {
|
|
564
|
-
id: generateId(),
|
|
565
|
-
type: "text",
|
|
566
|
-
switchingSets: [textSwitchingSet]
|
|
567
|
-
};
|
|
568
|
-
selectionSets.push(textSelectionSet);
|
|
569
|
-
}
|
|
570
|
-
return {
|
|
571
|
-
id: generateId(),
|
|
572
|
-
url: unresolved.url,
|
|
573
|
-
startTime: 0,
|
|
574
|
-
selectionSets
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Exponentially Weighted Moving Average (EWMA)
|
|
579
|
-
*
|
|
580
|
-
* Pure functional implementation of EWMA calculations.
|
|
581
|
-
* Based on Shaka Player's EWMA algorithm.
|
|
582
|
-
*/
|
|
583
|
-
/**
|
|
584
|
-
* Calculate alpha (decay factor) from half-life.
|
|
585
|
-
*
|
|
586
|
-
* Alpha determines how quickly old data "expires":
|
|
587
|
-
* - alpha close to 1 = slow decay (long memory)
|
|
588
|
-
* - alpha close to 0 = fast decay (short memory)
|
|
589
|
-
*
|
|
590
|
-
* @param halfLife - The quantity of prior samples (by weight) that make up
|
|
591
|
-
* half of the new estimate. Must be positive.
|
|
592
|
-
* @returns Alpha value between 0 and 1
|
|
593
|
-
*
|
|
594
|
-
* @example
|
|
595
|
-
* const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life
|
|
596
|
-
*/
|
|
597
|
-
function calculateAlpha(halfLife) {
|
|
598
|
-
return Math.exp(Math.log(.5) / halfLife);
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Calculate exponentially weighted moving average.
|
|
602
|
-
*
|
|
603
|
-
* Updates an estimate by blending a new value with the previous estimate,
|
|
604
|
-
* weighted by the sample duration. Longer samples have more influence.
|
|
605
|
-
*
|
|
606
|
-
* @param prevEstimate - Previous EWMA estimate
|
|
607
|
-
* @param value - New sample value to incorporate
|
|
608
|
-
* @param weight - Sample weight (typically duration in seconds)
|
|
609
|
-
* @param halfLife - Half-life for decay (typically 2-5 seconds)
|
|
610
|
-
* @returns Updated EWMA estimate
|
|
611
|
-
*
|
|
612
|
-
* @example
|
|
613
|
-
* let estimate = 0;
|
|
614
|
-
* estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample
|
|
615
|
-
* estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample
|
|
616
|
-
*/
|
|
617
|
-
function calculateEwma(prevEstimate, value, weight, halfLife) {
|
|
618
|
-
const adjAlpha = calculateAlpha(halfLife) ** weight;
|
|
619
|
-
return value * (1 - adjAlpha) + adjAlpha * prevEstimate;
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Apply zero-factor correction to EWMA estimate.
|
|
623
|
-
*
|
|
624
|
-
* The zero-factor correction compensates for bias when starting from zero.
|
|
625
|
-
* Without this correction, early estimates would be artificially low.
|
|
626
|
-
*
|
|
627
|
-
* As totalWeight increases, the correction factor approaches 1, meaning
|
|
628
|
-
* the estimate becomes more reliable and needs less correction.
|
|
629
|
-
*
|
|
630
|
-
* @param estimate - Raw EWMA estimate (uncorrected)
|
|
631
|
-
* @param totalWeight - Accumulated weight from all samples
|
|
632
|
-
* @param halfLife - Half-life used in EWMA calculation
|
|
633
|
-
* @returns Corrected estimate, or 0 if totalWeight is 0
|
|
634
|
-
*
|
|
635
|
-
* @example
|
|
636
|
-
* const raw = calculateEwma(0, 1_000_000, 1, 2);
|
|
637
|
-
* const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000
|
|
638
|
-
*/
|
|
639
|
-
function applyZeroFactor(estimate, totalWeight, halfLife) {
|
|
640
|
-
if (totalWeight === 0) return 0;
|
|
641
|
-
return estimate / (1 - calculateAlpha(halfLife) ** totalWeight);
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Default bandwidth estimator configuration.
|
|
645
|
-
*
|
|
646
|
-
* Values match Shaka Player defaults based on experimentation.
|
|
647
|
-
*/
|
|
648
|
-
const DEFAULT_BANDWIDTH_CONFIG = {
|
|
649
|
-
fastHalfLife: 2,
|
|
650
|
-
slowHalfLife: 5,
|
|
651
|
-
minTotalBytes: 128e3,
|
|
652
|
-
minBytes: 16e3,
|
|
653
|
-
minDuration: 5
|
|
654
|
-
};
|
|
655
|
-
/**
|
|
656
|
-
* Add a bandwidth sample from a segment download.
|
|
657
|
-
*
|
|
658
|
-
* Samples are filtered based on:
|
|
659
|
-
* - Minimum bytes (filters TTFB-dominated small segments)
|
|
660
|
-
* - Minimum duration (filters cached responses)
|
|
661
|
-
*
|
|
662
|
-
* Valid samples update both fast and slow EWMA estimates.
|
|
663
|
-
*
|
|
664
|
-
* @param state - Current estimator state
|
|
665
|
-
* @param durationMs - Download duration in milliseconds
|
|
666
|
-
* @param numBytes - Number of bytes downloaded
|
|
667
|
-
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
668
|
-
* @returns New estimator state with sample incorporated (or unchanged if filtered)
|
|
669
|
-
*
|
|
670
|
-
* @example
|
|
671
|
-
* let state = { fastEstimate: 0, fastTotalWeight: 0, ... };
|
|
672
|
-
* // Sample: 1MB in 1 second
|
|
673
|
-
* state = sampleBandwidth(state, 1000, 1_000_000);
|
|
674
|
-
*/
|
|
675
|
-
function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
676
|
-
const updatedBytesSampled = state.bytesSampled + numBytes;
|
|
677
|
-
if (numBytes < config.minBytes) return {
|
|
678
|
-
...state,
|
|
679
|
-
bytesSampled: updatedBytesSampled
|
|
680
|
-
};
|
|
681
|
-
if (durationMs < config.minDuration) return {
|
|
682
|
-
...state,
|
|
683
|
-
bytesSampled: updatedBytesSampled
|
|
684
|
-
};
|
|
685
|
-
const bandwidth = 8e3 * numBytes / durationMs;
|
|
686
|
-
const weight = durationMs / 1e3;
|
|
894
|
+
};
|
|
895
|
+
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
896
|
+
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
897
|
+
if (codecs?.video) track.codecs = [codecs.video];
|
|
898
|
+
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
899
|
+
if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
|
|
900
|
+
return track;
|
|
901
|
+
});
|
|
902
|
+
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
903
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
904
|
+
return {
|
|
905
|
+
type: "audio",
|
|
906
|
+
id: generateId(),
|
|
907
|
+
url: stream.uri,
|
|
908
|
+
bandwidth: stream.bandwidth,
|
|
909
|
+
mimeType: "audio/mp4",
|
|
910
|
+
codecs: codecs?.audio ? [codecs.audio] : [],
|
|
911
|
+
groupId: stream.audioGroupId || "default",
|
|
912
|
+
name: "Default",
|
|
913
|
+
sampleRate: 48e3,
|
|
914
|
+
channels: 2
|
|
915
|
+
};
|
|
916
|
+
});
|
|
917
|
+
const audioTracks = [...audioRenditions.map((rendition) => {
|
|
918
|
+
let audioCodecs;
|
|
919
|
+
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
920
|
+
const codecs = parseCodecs(stream.codecs);
|
|
921
|
+
if (codecs.audio) {
|
|
922
|
+
audioCodecs = [codecs.audio];
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const track = {
|
|
927
|
+
type: "audio",
|
|
928
|
+
id: generateId(),
|
|
929
|
+
url: rendition.uri ?? "",
|
|
930
|
+
groupId: rendition.groupId,
|
|
931
|
+
name: rendition.name,
|
|
932
|
+
mimeType: "audio/mp4",
|
|
933
|
+
bandwidth: 0,
|
|
934
|
+
sampleRate: 48e3,
|
|
935
|
+
channels: 2,
|
|
936
|
+
codecs: []
|
|
937
|
+
};
|
|
938
|
+
if (rendition.language) track.language = rendition.language;
|
|
939
|
+
if (audioCodecs) track.codecs = audioCodecs;
|
|
940
|
+
if (rendition.default) track.default = rendition.default;
|
|
941
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
942
|
+
return track;
|
|
943
|
+
}), ...audioOnlyTracks];
|
|
944
|
+
const textTracks = subtitleRenditions.map((rendition) => {
|
|
945
|
+
const track = {
|
|
946
|
+
type: "text",
|
|
947
|
+
id: generateId(),
|
|
948
|
+
url: rendition.uri,
|
|
949
|
+
groupId: rendition.groupId,
|
|
950
|
+
label: rendition.name,
|
|
951
|
+
kind: "subtitles",
|
|
952
|
+
mimeType: "text/vtt",
|
|
953
|
+
bandwidth: 0
|
|
954
|
+
};
|
|
955
|
+
if (rendition.language) track.language = rendition.language;
|
|
956
|
+
if (rendition.default && rendition.autoselect) track.default = true;
|
|
957
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
958
|
+
if (rendition.forced) track.forced = rendition.forced;
|
|
959
|
+
return track;
|
|
960
|
+
});
|
|
961
|
+
const selectionSets = [];
|
|
962
|
+
if (videoTracks.length > 0) {
|
|
963
|
+
const videoSwitchingSet = {
|
|
964
|
+
id: generateId(),
|
|
965
|
+
type: "video",
|
|
966
|
+
tracks: videoTracks
|
|
967
|
+
};
|
|
968
|
+
const videoSelectionSet = {
|
|
969
|
+
id: generateId(),
|
|
970
|
+
type: "video",
|
|
971
|
+
switchingSets: [videoSwitchingSet]
|
|
972
|
+
};
|
|
973
|
+
selectionSets.push(videoSelectionSet);
|
|
974
|
+
}
|
|
975
|
+
if (audioTracks.length > 0) {
|
|
976
|
+
const audioSwitchingSet = {
|
|
977
|
+
id: generateId(),
|
|
978
|
+
type: "audio",
|
|
979
|
+
tracks: audioTracks
|
|
980
|
+
};
|
|
981
|
+
const audioSelectionSet = {
|
|
982
|
+
id: generateId(),
|
|
983
|
+
type: "audio",
|
|
984
|
+
switchingSets: [audioSwitchingSet]
|
|
985
|
+
};
|
|
986
|
+
selectionSets.push(audioSelectionSet);
|
|
987
|
+
}
|
|
988
|
+
if (textTracks.length > 0) {
|
|
989
|
+
const textSwitchingSet = {
|
|
990
|
+
id: generateId(),
|
|
991
|
+
type: "text",
|
|
992
|
+
tracks: textTracks
|
|
993
|
+
};
|
|
994
|
+
const textSelectionSet = {
|
|
995
|
+
id: generateId(),
|
|
996
|
+
type: "text",
|
|
997
|
+
switchingSets: [textSwitchingSet]
|
|
998
|
+
};
|
|
999
|
+
selectionSets.push(textSelectionSet);
|
|
1000
|
+
}
|
|
687
1001
|
return {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
bytesSampled: updatedBytesSampled
|
|
1002
|
+
id: generateId(),
|
|
1003
|
+
url: unresolved.url,
|
|
1004
|
+
startTime: 0,
|
|
1005
|
+
selectionSets
|
|
693
1006
|
};
|
|
694
1007
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
* Returns the **minimum** of the fast and slow EWMA estimates.
|
|
699
|
-
* This provides the key asymmetric behavior:
|
|
700
|
-
* - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
|
|
701
|
-
* - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
|
|
702
|
-
*
|
|
703
|
-
* Uses default estimate until enough data has been sampled.
|
|
704
|
-
*
|
|
705
|
-
* @param state - Current estimator state
|
|
706
|
-
* @param defaultEstimate - Fallback estimate before sufficient samples (bps)
|
|
707
|
-
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
708
|
-
* @returns Bandwidth estimate in bits per second
|
|
709
|
-
*
|
|
710
|
-
* @example
|
|
711
|
-
* const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
|
|
712
|
-
*/
|
|
713
|
-
function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
714
|
-
if (state.bytesSampled < config.minTotalBytes) return defaultEstimate;
|
|
715
|
-
const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
|
|
716
|
-
const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
|
|
717
|
-
return Math.min(fastEstimate, slowEstimate);
|
|
718
|
-
}
|
|
1008
|
+
|
|
1009
|
+
//#endregion
|
|
1010
|
+
//#region ../spf/dist/dev/core/abr/quality-selection.js
|
|
719
1011
|
/**
|
|
720
1012
|
* Default quality selection configuration.
|
|
721
1013
|
* Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
|
|
@@ -764,125 +1056,53 @@ function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG
|
|
|
764
1056
|
function hasHigherResolution(trackA, trackB) {
|
|
765
1057
|
return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
|
|
766
1058
|
}
|
|
1059
|
+
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region ../spf/dist/dev/core/buffer/back-buffer.js
|
|
767
1062
|
/**
|
|
768
|
-
* Default back buffer configuration.
|
|
769
|
-
*/
|
|
770
|
-
const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
|
|
771
|
-
/**
|
|
772
|
-
* Calculate back buffer flush point.
|
|
773
|
-
*
|
|
774
|
-
* Determines where to flush old segments from the back buffer.
|
|
775
|
-
* Keeps a fixed number of segments behind the current playback position.
|
|
776
|
-
*
|
|
777
|
-
* Algorithm:
|
|
778
|
-
* 1. Find segments before currentTime
|
|
779
|
-
* 2. Count back N segments (keepSegments)
|
|
780
|
-
* 3. Return startTime of segment N+1 back (flush everything before this)
|
|
781
|
-
*
|
|
782
|
-
* @param segments - Available segments (should be sorted by startTime)
|
|
783
|
-
* @param currentTime - Current playback position in seconds
|
|
784
|
-
* @param config - Optional back buffer configuration
|
|
785
|
-
* @returns Time in seconds to flush up to (flush range: [0, flushEnd))
|
|
786
|
-
*
|
|
787
|
-
* @example
|
|
788
|
-
* const segments = [
|
|
789
|
-
* { startTime: 0, duration: 6, ... },
|
|
790
|
-
* { startTime: 6, duration: 6, ... },
|
|
791
|
-
* { startTime: 12, duration: 6, ... },
|
|
792
|
-
* { startTime: 18, duration: 6, ... },
|
|
793
|
-
* ];
|
|
794
|
-
*
|
|
795
|
-
* // Playing at 18s, keep 2 segments
|
|
796
|
-
* const flushEnd = calculateBackBufferFlushPoint(segments, 18);
|
|
797
|
-
* // Returns 6 (flush [0, 6), keep [6-18))
|
|
798
|
-
*/
|
|
799
|
-
function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
|
|
800
|
-
if (segments.length === 0) return 0;
|
|
801
|
-
const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
|
|
802
|
-
if (segmentsBefore.length === 0) return 0;
|
|
803
|
-
const segmentsToFlush = segmentsBefore.length - config.keepSegments;
|
|
804
|
-
if (segmentsToFlush <= 0) return 0;
|
|
805
|
-
if (segmentsToFlush >= segmentsBefore.length) return currentTime;
|
|
806
|
-
return segmentsBefore[segmentsToFlush].startTime;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Default forward buffer configuration.
|
|
810
|
-
*/
|
|
811
|
-
const DEFAULT_FORWARD_BUFFER_CONFIG = { bufferDuration: 30 };
|
|
812
|
-
/**
|
|
813
|
-
* Get segments that need to be loaded for forward buffer.
|
|
814
|
-
*
|
|
815
|
-
* Determines which segments to load to maintain target buffer duration.
|
|
816
|
-
* Handles discontiguous buffering (gaps after seeks).
|
|
817
|
-
*
|
|
818
|
-
* Algorithm:
|
|
819
|
-
* 1. Calculate target time: currentTime + bufferDuration
|
|
820
|
-
* 2. Find all segments in range [currentTime, targetTime)
|
|
821
|
-
* 3. Filter out segments already buffered at that time position
|
|
822
|
-
* 4. Return segments to load (fills gaps + extends to target)
|
|
823
|
-
*
|
|
824
|
-
* @param segments - All available segments from playlist
|
|
825
|
-
* @param bufferedSegments - Segments already buffered (ordered by startTime)
|
|
826
|
-
* @param currentTime - Current playback position in seconds
|
|
827
|
-
* @param config - Optional forward buffer configuration
|
|
828
|
-
* @returns Array of segments to load (empty if buffer is sufficient)
|
|
829
|
-
*
|
|
830
|
-
* @example
|
|
831
|
-
* // After seek: buffered [0-12, 18-30], playing at 7s
|
|
832
|
-
* const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });
|
|
833
|
-
* // Returns [seg-12, seg-30] (fills gap, extends to target 31s)
|
|
834
|
-
*/
|
|
835
|
-
/**
|
|
836
|
-
* Calculate the start time from which to flush forward buffer content.
|
|
837
|
-
*
|
|
838
|
-
* Content that starts at or beyond `currentTime + bufferDuration` is no
|
|
839
|
-
* longer needed for the current playback position and should be removed
|
|
840
|
-
* from the SourceBuffer. This prevents unbounded accumulation of scattered
|
|
841
|
-
* SourceBuffer content after seeks, which can cause QuotaExceededError on
|
|
842
|
-
* long-form content.
|
|
843
|
-
*
|
|
844
|
-
* Returns `Infinity` when nothing needs flushing (no buffered segments
|
|
845
|
-
* exist beyond the threshold).
|
|
846
|
-
*
|
|
847
|
-
* @param bufferedSegments - Segments currently tracked in the buffer model
|
|
848
|
-
* @param currentTime - Current playback position in seconds
|
|
849
|
-
* @param config - Optional forward buffer configuration
|
|
850
|
-
* @returns Start time to flush from (flush range: [flushStart, Infinity)),
|
|
851
|
-
* or Infinity if no flush is needed
|
|
852
|
-
*
|
|
853
|
-
* @example
|
|
854
|
-
* // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30
|
|
855
|
-
* const flushStart = calculateForwardFlushPoint(segments, 0);
|
|
856
|
-
* // Returns 30 — flush [30, Infinity), keep [0, 30)
|
|
857
|
-
*/
|
|
858
|
-
function calculateForwardFlushPoint(bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
|
|
859
|
-
if (bufferedSegments.length === 0) return Infinity;
|
|
860
|
-
const threshold = currentTime + config.bufferDuration;
|
|
861
|
-
const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);
|
|
862
|
-
if (beyond.length === 0) return Infinity;
|
|
863
|
-
return Math.min(...beyond.map((seg) => seg.startTime));
|
|
864
|
-
}
|
|
865
|
-
function getSegmentsToLoad(segments, bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
|
|
866
|
-
if (segments.length === 0) return [];
|
|
867
|
-
const targetTime = currentTime + config.bufferDuration;
|
|
868
|
-
const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));
|
|
869
|
-
return segments.filter((seg) => {
|
|
870
|
-
const segmentEnd = seg.startTime + seg.duration;
|
|
871
|
-
const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;
|
|
872
|
-
const isNotBuffered = !bufferedStartTimes.has(seg.startTime);
|
|
873
|
-
return isInRange && isNotBuffered;
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
function isResolvedTrack(track) {
|
|
877
|
-
return "segments" in track;
|
|
878
|
-
}
|
|
879
|
-
/**
|
|
880
|
-
* Check if a presentation has duration (at least one track resolved).
|
|
881
|
-
* Narrows type to include required duration.
|
|
1063
|
+
* Default back buffer configuration.
|
|
882
1064
|
*/
|
|
883
|
-
|
|
884
|
-
|
|
1065
|
+
const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
|
|
1066
|
+
/**
|
|
1067
|
+
* Calculate back buffer flush point.
|
|
1068
|
+
*
|
|
1069
|
+
* Determines where to flush old segments from the back buffer.
|
|
1070
|
+
* Keeps a fixed number of segments behind the current playback position.
|
|
1071
|
+
*
|
|
1072
|
+
* Algorithm:
|
|
1073
|
+
* 1. Find segments before currentTime
|
|
1074
|
+
* 2. Count back N segments (keepSegments)
|
|
1075
|
+
* 3. Return startTime of segment N+1 back (flush everything before this)
|
|
1076
|
+
*
|
|
1077
|
+
* @param segments - Available segments (should be sorted by startTime)
|
|
1078
|
+
* @param currentTime - Current playback position in seconds
|
|
1079
|
+
* @param config - Optional back buffer configuration
|
|
1080
|
+
* @returns Time in seconds to flush up to (flush range: [0, flushEnd))
|
|
1081
|
+
*
|
|
1082
|
+
* @example
|
|
1083
|
+
* const segments = [
|
|
1084
|
+
* { startTime: 0, duration: 6, ... },
|
|
1085
|
+
* { startTime: 6, duration: 6, ... },
|
|
1086
|
+
* { startTime: 12, duration: 6, ... },
|
|
1087
|
+
* { startTime: 18, duration: 6, ... },
|
|
1088
|
+
* ];
|
|
1089
|
+
*
|
|
1090
|
+
* // Playing at 18s, keep 2 segments
|
|
1091
|
+
* const flushEnd = calculateBackBufferFlushPoint(segments, 18);
|
|
1092
|
+
* // Returns 6 (flush [0, 6), keep [6-18))
|
|
1093
|
+
*/
|
|
1094
|
+
function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
|
|
1095
|
+
if (segments.length === 0) return 0;
|
|
1096
|
+
const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
|
|
1097
|
+
if (segmentsBefore.length === 0) return 0;
|
|
1098
|
+
const segmentsToFlush = segmentsBefore.length - config.keepSegments;
|
|
1099
|
+
if (segmentsToFlush <= 0) return 0;
|
|
1100
|
+
if (segmentsToFlush >= segmentsBefore.length) return currentTime;
|
|
1101
|
+
return segmentsBefore[segmentsToFlush].startTime;
|
|
885
1102
|
}
|
|
1103
|
+
|
|
1104
|
+
//#endregion
|
|
1105
|
+
//#region ../spf/dist/dev/dom/media/mediasource-setup.js
|
|
886
1106
|
/**
|
|
887
1107
|
* MediaSource Setup
|
|
888
1108
|
*
|
|
@@ -943,13 +1163,13 @@ function attachMediaSource(mediaSource, mediaElement) {
|
|
|
943
1163
|
if (supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource) {
|
|
944
1164
|
mediaElement.disableRemotePlayback = true;
|
|
945
1165
|
mediaElement.srcObject = mediaSource;
|
|
946
|
-
const detach
|
|
1166
|
+
const detach = () => {
|
|
947
1167
|
mediaElement.srcObject = null;
|
|
948
1168
|
mediaElement.load();
|
|
949
1169
|
};
|
|
950
1170
|
return {
|
|
951
1171
|
url: "",
|
|
952
|
-
detach
|
|
1172
|
+
detach
|
|
953
1173
|
};
|
|
954
1174
|
}
|
|
955
1175
|
const url = URL.createObjectURL(mediaSource);
|
|
@@ -1032,99 +1252,9 @@ function isCodecSupported(mimeCodec) {
|
|
|
1032
1252
|
if (!supportsMediaSource()) return false;
|
|
1033
1253
|
return MediaSource.isTypeSupported(mimeCodec);
|
|
1034
1254
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
* `AsyncIterable<Uint8Array>` that yields chunks no smaller than
|
|
1039
|
-
* `minChunkSize` bytes. Smaller network chunks are accumulated and yielded
|
|
1040
|
-
* together once the threshold is met. Any remainder is flushed on stream end.
|
|
1041
|
-
*
|
|
1042
|
-
* Errors from the underlying stream propagate naturally — the reader lock is
|
|
1043
|
-
* always released via `finally`.
|
|
1044
|
-
*/
|
|
1045
|
-
var ChunkedStreamIterable = class {
|
|
1046
|
-
minChunkSize;
|
|
1047
|
-
#readableStream;
|
|
1048
|
-
constructor(readableStream, { minChunkSize = DEFAULT_MIN_CHUNK_SIZE } = {}) {
|
|
1049
|
-
this.#readableStream = readableStream;
|
|
1050
|
-
this.minChunkSize = minChunkSize;
|
|
1051
|
-
}
|
|
1052
|
-
async *[Symbol.asyncIterator]() {
|
|
1053
|
-
let pending;
|
|
1054
|
-
const reader = this.#readableStream.getReader();
|
|
1055
|
-
try {
|
|
1056
|
-
while (true) {
|
|
1057
|
-
const { done, value } = await reader.read();
|
|
1058
|
-
if (done) {
|
|
1059
|
-
if (pending) yield pending;
|
|
1060
|
-
break;
|
|
1061
|
-
}
|
|
1062
|
-
pending = pending ? concat(pending, value) : value;
|
|
1063
|
-
if (pending.length >= this.minChunkSize) {
|
|
1064
|
-
yield pending;
|
|
1065
|
-
pending = void 0;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
} finally {
|
|
1069
|
-
reader.releaseLock();
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
function concat(a, b) {
|
|
1074
|
-
const result = new Uint8Array(a.length + b.length);
|
|
1075
|
-
result.set(a);
|
|
1076
|
-
result.set(b, a.length);
|
|
1077
|
-
return result;
|
|
1078
|
-
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Fetch resolvable from AddressableObject.
|
|
1081
|
-
*
|
|
1082
|
-
* Handles byte range requests if byteRange is present.
|
|
1083
|
-
* Returns native fetch Response for composability (can extract text, stream, etc.).
|
|
1084
|
-
*
|
|
1085
|
-
* @param addressable - Resource to fetch (url + optional byteRange)
|
|
1086
|
-
* @returns Promise resolving to Response
|
|
1087
|
-
*
|
|
1088
|
-
* @example
|
|
1089
|
-
* const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
|
|
1090
|
-
* const text = await getResponseText(response);
|
|
1091
|
-
*
|
|
1092
|
-
* @example
|
|
1093
|
-
* // With byte range
|
|
1094
|
-
* const response = await fetchResolvable({
|
|
1095
|
-
* url: 'https://example.com/file.mp4',
|
|
1096
|
-
* byteRange: { start: 1000, end: 1999 }
|
|
1097
|
-
* });
|
|
1098
|
-
*/
|
|
1099
|
-
async function fetchResolvable(addressable, options) {
|
|
1100
|
-
const headers = new Headers(options?.headers);
|
|
1101
|
-
if (addressable.byteRange) {
|
|
1102
|
-
const { start, end } = addressable.byteRange;
|
|
1103
|
-
headers.set("Range", `bytes=${start}-${end}`);
|
|
1104
|
-
}
|
|
1105
|
-
const request = new Request(addressable.url, {
|
|
1106
|
-
method: "GET",
|
|
1107
|
-
headers,
|
|
1108
|
-
...options
|
|
1109
|
-
});
|
|
1110
|
-
return fetch(request);
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Extract text from Response.
|
|
1114
|
-
*
|
|
1115
|
-
* Accepts minimal Response-like object (just needs text() method).
|
|
1116
|
-
* Returns promise from response.text().
|
|
1117
|
-
*
|
|
1118
|
-
* @param response - Response-like object with text() method
|
|
1119
|
-
* @returns Promise resolving to text content
|
|
1120
|
-
*
|
|
1121
|
-
* @example
|
|
1122
|
-
* const response = await fetchResolvable(addressable);
|
|
1123
|
-
* const text = await getResponseText(response);
|
|
1124
|
-
*/
|
|
1125
|
-
function getResponseText(response) {
|
|
1126
|
-
return response.text();
|
|
1127
|
-
}
|
|
1255
|
+
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region ../spf/dist/dev/core/events/create-event-stream.js
|
|
1128
1258
|
/**
|
|
1129
1259
|
* Minimal event stream with Observable-like shape.
|
|
1130
1260
|
*
|
|
@@ -1168,73 +1298,9 @@ function createEventStream() {
|
|
|
1168
1298
|
}
|
|
1169
1299
|
};
|
|
1170
1300
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
* Emits an array of latest values whenever any source emits.
|
|
1175
|
-
* Only emits after all sources have emitted at least once.
|
|
1176
|
-
*
|
|
1177
|
-
* Supports selector-based subscriptions (fires only when the selected value
|
|
1178
|
-
* changes, per the optional equalityFn) mirroring the createState API.
|
|
1179
|
-
*
|
|
1180
|
-
* @param sources - Array of Observable sources
|
|
1181
|
-
* @returns Combined Observable
|
|
1182
|
-
*
|
|
1183
|
-
* @example
|
|
1184
|
-
* ```ts
|
|
1185
|
-
* const state = createState({ count: 0 });
|
|
1186
|
-
* const events = createEventStream<Action>();
|
|
1187
|
-
*
|
|
1188
|
-
* combineLatest([state, events]).subscribe(([state, event]) => {
|
|
1189
|
-
* if (event.type === 'PLAY' && state.count > 0) {
|
|
1190
|
-
* // React to event + state condition
|
|
1191
|
-
* }
|
|
1192
|
-
* });
|
|
1193
|
-
* ```
|
|
1194
|
-
*
|
|
1195
|
-
* @example Selector subscription
|
|
1196
|
-
* ```ts
|
|
1197
|
-
* combineLatest([state, owners]).subscribe(
|
|
1198
|
-
* ([s, o]) => deriveKey(s, o),
|
|
1199
|
-
* (key) => { ... },
|
|
1200
|
-
* { equalityFn: keyEq }
|
|
1201
|
-
* );
|
|
1202
|
-
* ```
|
|
1203
|
-
*/
|
|
1204
|
-
function combineLatest(sources) {
|
|
1205
|
-
const subscribeToSources = (listener) => {
|
|
1206
|
-
const latest = new Array(sources.length);
|
|
1207
|
-
const hasValue = new Array(sources.length).fill(false);
|
|
1208
|
-
const unsubscribers = [];
|
|
1209
|
-
for (let i = 0; i < sources.length; i++) {
|
|
1210
|
-
const unsubscribe = sources[i].subscribe((value) => {
|
|
1211
|
-
latest[i] = value;
|
|
1212
|
-
hasValue[i] = true;
|
|
1213
|
-
if (hasValue.every((has) => has)) listener([...latest]);
|
|
1214
|
-
});
|
|
1215
|
-
unsubscribers.push(unsubscribe);
|
|
1216
|
-
}
|
|
1217
|
-
return () => {
|
|
1218
|
-
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
1219
|
-
};
|
|
1220
|
-
};
|
|
1221
|
-
return { subscribe(listenerOrSelector, maybeListener, options) {
|
|
1222
|
-
if (maybeListener === void 0) return subscribeToSources(listenerOrSelector);
|
|
1223
|
-
const selector = listenerOrSelector;
|
|
1224
|
-
const listener = maybeListener;
|
|
1225
|
-
const equalityFn = options?.equalityFn ?? Object.is;
|
|
1226
|
-
let prevSelected;
|
|
1227
|
-
let initialized = false;
|
|
1228
|
-
return subscribeToSources((values) => {
|
|
1229
|
-
const nextSelected = selector(values);
|
|
1230
|
-
if (!initialized || !equalityFn(prevSelected, nextSelected)) {
|
|
1231
|
-
prevSelected = nextSelected;
|
|
1232
|
-
initialized = true;
|
|
1233
|
-
listener(nextSelected);
|
|
1234
|
-
}
|
|
1235
|
-
});
|
|
1236
|
-
} };
|
|
1237
|
-
}
|
|
1301
|
+
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region ../spf/dist/dev/core/features/resolve-presentation.js
|
|
1238
1304
|
/**
|
|
1239
1305
|
* Type guard to check if presentation is unresolved.
|
|
1240
1306
|
*/
|
|
@@ -1325,6 +1391,9 @@ function resolvePresentation({ state, events }) {
|
|
|
1325
1391
|
cleanup();
|
|
1326
1392
|
};
|
|
1327
1393
|
}
|
|
1394
|
+
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region ../spf/dist/dev/core/features/quality-switching.js
|
|
1328
1397
|
/**
|
|
1329
1398
|
* Default quality switching configuration.
|
|
1330
1399
|
*/
|
|
@@ -1385,6 +1454,9 @@ function switchQuality({ state }, config = {}) {
|
|
|
1385
1454
|
state.patch({ selectedVideoTrackId: optimal.id });
|
|
1386
1455
|
});
|
|
1387
1456
|
}
|
|
1457
|
+
|
|
1458
|
+
//#endregion
|
|
1459
|
+
//#region ../spf/dist/dev/core/utils/track-selection.js
|
|
1388
1460
|
/**
|
|
1389
1461
|
* Map track type to selected track ID property key in state.
|
|
1390
1462
|
*/
|
|
@@ -1411,6 +1483,9 @@ function getSelectedTrack(state, type) {
|
|
|
1411
1483
|
const trackId = state[SelectedTrackIdKeyByType[type]];
|
|
1412
1484
|
return presentation.selectionSets.find(({ type: selectionSetType }) => selectionSetType === type)?.switchingSets[0]?.tracks.find(({ id }) => id === trackId);
|
|
1413
1485
|
}
|
|
1486
|
+
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region ../spf/dist/dev/dom/features/segment-loader-actor.js
|
|
1414
1489
|
/**
|
|
1415
1490
|
* Creates a SegmentLoaderActor for one track type (video or audio).
|
|
1416
1491
|
*
|
|
@@ -1600,6 +1675,9 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
|
|
|
1600
1675
|
}
|
|
1601
1676
|
};
|
|
1602
1677
|
}
|
|
1678
|
+
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region ../spf/dist/dev/dom/features/load-segments.js
|
|
1603
1681
|
const ActorKeyByType = {
|
|
1604
1682
|
video: "videoBufferActor",
|
|
1605
1683
|
audio: "audioBufferActor"
|
|
@@ -1750,7 +1828,7 @@ function loadSegments({ state, owners }, config) {
|
|
|
1750
1828
|
const segmentLoaderActorExists = !!currentSegmentLoader;
|
|
1751
1829
|
segmentsCanLoad.patch(trackResolved && segmentLoaderActorExists);
|
|
1752
1830
|
});
|
|
1753
|
-
const unsubscribeShouldLoadSegments = combineLatest([segmentsCanLoad, state]).subscribe(([segmentsCanLoad
|
|
1831
|
+
const unsubscribeShouldLoadSegments = combineLatest([segmentsCanLoad, state]).subscribe(([segmentsCanLoad, state]) => selectLoadingInputs([segmentsCanLoad, state], type), ({ preload, playbackInitiated, currentTime, track }) => {
|
|
1754
1832
|
if (!(preload === "auto" || !!playbackInitiated))
|
|
1755
1833
|
/** @ts-expect-error */
|
|
1756
1834
|
segmentLoader.current?.send({
|
|
@@ -1772,6 +1850,9 @@ function loadSegments({ state, owners }, config) {
|
|
|
1772
1850
|
unsubActorLifecycle();
|
|
1773
1851
|
};
|
|
1774
1852
|
}
|
|
1853
|
+
|
|
1854
|
+
//#endregion
|
|
1855
|
+
//#region ../spf/dist/dev/dom/text/parse-vtt-segment.js
|
|
1775
1856
|
/**
|
|
1776
1857
|
* Parse a VTT segment using browser's native parser.
|
|
1777
1858
|
*
|
|
@@ -1823,6 +1904,9 @@ function parseVttSegment(url) {
|
|
|
1823
1904
|
function destroyVttParser() {
|
|
1824
1905
|
dummyVideo = null;
|
|
1825
1906
|
}
|
|
1907
|
+
|
|
1908
|
+
//#endregion
|
|
1909
|
+
//#region ../spf/dist/dev/dom/features/load-text-track-cues.js
|
|
1826
1910
|
function isDuplicateCue(cue, textTrack) {
|
|
1827
1911
|
const { cues } = textTrack;
|
|
1828
1912
|
if (!cues) return false;
|
|
@@ -1964,6 +2048,9 @@ function loadTextTrackCues({ state, owners }) {
|
|
|
1964
2048
|
cleanup();
|
|
1965
2049
|
};
|
|
1966
2050
|
}
|
|
2051
|
+
|
|
2052
|
+
//#endregion
|
|
2053
|
+
//#region ../spf/dist/dev/dom/features/track-current-time.js
|
|
1967
2054
|
/**
|
|
1968
2055
|
* Track current playback position from the media element.
|
|
1969
2056
|
*
|
|
@@ -2003,6 +2090,9 @@ function trackCurrentTime({ state, owners }) {
|
|
|
2003
2090
|
unsubscribe();
|
|
2004
2091
|
};
|
|
2005
2092
|
}
|
|
2093
|
+
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region ../spf/dist/dev/dom/features/track-playback-initiated.js
|
|
2006
2096
|
/**
|
|
2007
2097
|
* Track whether playback has been initiated by the user.
|
|
2008
2098
|
*
|
|
@@ -2051,6 +2141,9 @@ function trackPlaybackInitiated({ state, owners, events }) {
|
|
|
2051
2141
|
unsubscribeOwners();
|
|
2052
2142
|
};
|
|
2053
2143
|
}
|
|
2144
|
+
|
|
2145
|
+
//#endregion
|
|
2146
|
+
//#region ../spf/dist/dev/dom/media/append-segment.js
|
|
2054
2147
|
/**
|
|
2055
2148
|
* Append media data to a SourceBuffer.
|
|
2056
2149
|
*
|
|
@@ -2106,6 +2199,9 @@ async function appendChunk(sourceBuffer, data) {
|
|
|
2106
2199
|
}
|
|
2107
2200
|
});
|
|
2108
2201
|
}
|
|
2202
|
+
|
|
2203
|
+
//#endregion
|
|
2204
|
+
//#region ../spf/dist/dev/dom/media/buffer-flusher.js
|
|
2109
2205
|
/**
|
|
2110
2206
|
* Buffer flusher helper (P12)
|
|
2111
2207
|
*
|
|
@@ -2156,6 +2252,9 @@ async function flushBuffer(sourceBuffer, start, end) {
|
|
|
2156
2252
|
}
|
|
2157
2253
|
});
|
|
2158
2254
|
}
|
|
2255
|
+
|
|
2256
|
+
//#endregion
|
|
2257
|
+
//#region ../spf/dist/dev/core/features/calculate-presentation-duration.js
|
|
2159
2258
|
/**
|
|
2160
2259
|
* Check if we can calculate presentation duration (have required data).
|
|
2161
2260
|
*/
|
|
@@ -2198,6 +2297,9 @@ function calculatePresentationDuration({ state }) {
|
|
|
2198
2297
|
} });
|
|
2199
2298
|
});
|
|
2200
2299
|
}
|
|
2300
|
+
|
|
2301
|
+
//#endregion
|
|
2302
|
+
//#region ../spf/dist/dev/core/task.js
|
|
2201
2303
|
/**
|
|
2202
2304
|
* Generic reusable task that wraps an async run function.
|
|
2203
2305
|
*
|
|
@@ -2313,6 +2415,9 @@ var SerialRunner = class {
|
|
|
2313
2415
|
this.abortAll();
|
|
2314
2416
|
}
|
|
2315
2417
|
};
|
|
2418
|
+
|
|
2419
|
+
//#endregion
|
|
2420
|
+
//#region ../spf/dist/dev/core/features/resolve-track.js
|
|
2316
2421
|
function canResolve(state, config) {
|
|
2317
2422
|
const track = getSelectedTrack(state, config.type);
|
|
2318
2423
|
if (!track) return false;
|
|
@@ -2378,6 +2483,9 @@ function resolveTrack({ state, events }, config) {
|
|
|
2378
2483
|
cleanup();
|
|
2379
2484
|
};
|
|
2380
2485
|
}
|
|
2486
|
+
|
|
2487
|
+
//#endregion
|
|
2488
|
+
//#region ../spf/dist/dev/core/features/select-tracks.js
|
|
2381
2489
|
/**
|
|
2382
2490
|
* Pick text track to activate.
|
|
2383
2491
|
*
|
|
@@ -2518,6 +2626,9 @@ function selectTextTrack({ state }, config = { type: "text" }) {
|
|
|
2518
2626
|
}
|
|
2519
2627
|
});
|
|
2520
2628
|
}
|
|
2629
|
+
|
|
2630
|
+
//#endregion
|
|
2631
|
+
//#region ../spf/dist/dev/dom/features/end-of-stream.js
|
|
2521
2632
|
/**
|
|
2522
2633
|
* Check if the last segment of a track has been appended to a SourceBuffer.
|
|
2523
2634
|
*
|
|
@@ -2678,6 +2789,9 @@ function endOfStream({ state, owners }) {
|
|
|
2678
2789
|
cleanupCombineLatest();
|
|
2679
2790
|
};
|
|
2680
2791
|
}
|
|
2792
|
+
|
|
2793
|
+
//#endregion
|
|
2794
|
+
//#region ../spf/dist/dev/dom/features/setup-mediasource.js
|
|
2681
2795
|
/**
|
|
2682
2796
|
* Check if we have the minimum requirements to create MediaSource.
|
|
2683
2797
|
*/
|
|
@@ -2724,6 +2838,9 @@ function setupMediaSource({ state, owners }) {
|
|
|
2724
2838
|
unsubscribe();
|
|
2725
2839
|
};
|
|
2726
2840
|
}
|
|
2841
|
+
|
|
2842
|
+
//#endregion
|
|
2843
|
+
//#region ../spf/dist/dev/dom/media/source-buffer-actor.js
|
|
2727
2844
|
/**
|
|
2728
2845
|
* Thrown when a message is sent to the actor in a state that does not
|
|
2729
2846
|
* accept messages (currently: 'updating').
|
|
@@ -2900,6 +3017,9 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
|
2900
3017
|
}
|
|
2901
3018
|
};
|
|
2902
3019
|
}
|
|
3020
|
+
|
|
3021
|
+
//#endregion
|
|
3022
|
+
//#region ../spf/dist/dev/dom/features/setup-sourcebuffer.js
|
|
2903
3023
|
/**
|
|
2904
3024
|
* Build MIME codec string from track metadata.
|
|
2905
3025
|
*
|
|
@@ -2958,6 +3078,9 @@ function setupSourceBuffers({ state, owners }) {
|
|
|
2958
3078
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
2959
3079
|
});
|
|
2960
3080
|
}
|
|
3081
|
+
|
|
3082
|
+
//#endregion
|
|
3083
|
+
//#region ../spf/dist/dev/dom/features/setup-text-tracks.js
|
|
2961
3084
|
/**
|
|
2962
3085
|
* Get all text tracks from presentation.
|
|
2963
3086
|
*/
|
|
@@ -3039,6 +3162,9 @@ function setupTextTracks({ state, owners }) {
|
|
|
3039
3162
|
unsubscribe();
|
|
3040
3163
|
};
|
|
3041
3164
|
}
|
|
3165
|
+
|
|
3166
|
+
//#endregion
|
|
3167
|
+
//#region ../spf/dist/dev/dom/features/sync-selected-text-track-from-dom.js
|
|
3042
3168
|
/**
|
|
3043
3169
|
* Sync selectedTextTrackId from DOM text track mode changes.
|
|
3044
3170
|
*
|
|
@@ -3095,6 +3221,9 @@ function syncSelectedTextTrackFromDom({ state, owners }) {
|
|
|
3095
3221
|
unsubscribe();
|
|
3096
3222
|
};
|
|
3097
3223
|
}
|
|
3224
|
+
|
|
3225
|
+
//#endregion
|
|
3226
|
+
//#region ../spf/dist/dev/dom/features/sync-text-track-modes.js
|
|
3098
3227
|
/**
|
|
3099
3228
|
* Check if we can sync text track modes.
|
|
3100
3229
|
*
|
|
@@ -3126,6 +3255,9 @@ function syncTextTrackModes({ state, owners }) {
|
|
|
3126
3255
|
else trackElement.track.mode = "hidden";
|
|
3127
3256
|
});
|
|
3128
3257
|
}
|
|
3258
|
+
|
|
3259
|
+
//#endregion
|
|
3260
|
+
//#region ../spf/dist/dev/dom/features/update-duration.js
|
|
3129
3261
|
/**
|
|
3130
3262
|
* Check if we can update MediaSource duration (have required data).
|
|
3131
3263
|
*/
|
|
@@ -3190,6 +3322,9 @@ function updateDuration({ state, owners }) {
|
|
|
3190
3322
|
unsubscribe();
|
|
3191
3323
|
};
|
|
3192
3324
|
}
|
|
3325
|
+
|
|
3326
|
+
//#endregion
|
|
3327
|
+
//#region ../spf/dist/dev/dom/playback-engine/engine.js
|
|
3193
3328
|
/**
|
|
3194
3329
|
* Create a POC playback engine.
|
|
3195
3330
|
*
|
|
@@ -3341,6 +3476,9 @@ function createPlaybackEngine(config = {}) {
|
|
|
3341
3476
|
}
|
|
3342
3477
|
};
|
|
3343
3478
|
}
|
|
3479
|
+
|
|
3480
|
+
//#endregion
|
|
3481
|
+
//#region ../spf/dist/dev/dom/playback-engine/adapter.js
|
|
3344
3482
|
/**
|
|
3345
3483
|
* HTMLMediaElement-compatible adapter for the SPF playback engine.
|
|
3346
3484
|
*
|