@videojs/spf 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/README.md +16 -40
- package/dist/default/all.js +16 -0
- package/dist/default/core/abr/bandwidth-estimator.js +96 -0
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/default/core/abr/ewma.js +71 -0
- package/dist/default/core/abr/ewma.js.map +1 -0
- package/dist/default/core/abr/quality-selection.js +53 -0
- package/dist/default/core/abr/quality-selection.js.map +1 -0
- package/dist/default/core/buffer/back-buffer.js +46 -0
- package/dist/default/core/buffer/back-buffer.js.map +1 -0
- package/dist/default/core/buffer/forward-buffer.js +73 -0
- package/dist/default/core/buffer/forward-buffer.js.map +1 -0
- package/dist/default/core/events/create-event-stream.js +48 -0
- package/dist/default/core/events/create-event-stream.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +51 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +68 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +99 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +77 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +147 -0
- package/dist/default/core/features/select-tracks.js.map +1 -0
- package/dist/default/core/hls/parse-attributes.js +134 -0
- package/dist/default/core/hls/parse-attributes.js.map +1 -0
- package/dist/default/core/hls/parse-media-playlist.js +83 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +215 -0
- package/dist/default/core/hls/parse-multivariant.js.map +1 -0
- package/dist/default/core/hls/resolve-url.js +11 -0
- package/dist/default/core/hls/resolve-url.js.map +1 -0
- package/dist/default/core/reactive/combine-latest.js +72 -0
- package/dist/default/core/reactive/combine-latest.js.map +1 -0
- package/dist/default/core/state/create-state.js +143 -0
- package/dist/default/core/state/create-state.js.map +1 -0
- package/dist/default/core/task.js +122 -0
- package/dist/default/core/task.js.map +1 -0
- package/dist/default/core/types/index.js +15 -0
- package/dist/default/core/types/index.js.map +1 -0
- package/dist/default/core/utils/generate-id.js +21 -0
- package/dist/default/core/utils/generate-id.js.map +1 -0
- package/dist/default/core/utils/track-selection.js +31 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +169 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +188 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +151 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +197 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +55 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +70 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/setup-text-tracks.js +88 -0
- package/dist/default/dom/features/setup-text-tracks.js.map +1 -0
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js +63 -0
- package/dist/default/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
- package/dist/default/dom/features/sync-text-track-modes.js +38 -0
- package/dist/default/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +46 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +55 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/{dom/index.js → default/dom/features/track-playback-rate.js} +2 -4
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +72 -0
- package/dist/default/dom/features/update-duration.js.map +1 -0
- package/dist/default/dom/media/append-segment.js +60 -0
- package/dist/default/dom/media/append-segment.js.map +1 -0
- package/dist/default/dom/media/buffer-flusher.js +55 -0
- package/dist/default/dom/media/buffer-flusher.js.map +1 -0
- package/dist/default/dom/media/mediasource-setup.js +154 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +186 -0
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/default/dom/network/fetch.js +56 -0
- package/dist/default/dom/network/fetch.js.map +1 -0
- package/dist/default/dom/playback-engine/adapter.js +95 -0
- package/dist/default/dom/playback-engine/adapter.js.map +1 -0
- package/dist/default/dom/playback-engine/engine.js +176 -0
- package/dist/default/dom/playback-engine/engine.js.map +1 -0
- package/dist/default/dom/playback-engine.js +4 -0
- package/dist/default/dom/text/parse-vtt-segment.js +56 -0
- package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/default/dom.js +11 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +2 -2
- package/dist/default/index.js.map +1 -0
- package/dist/dev/all.js +16 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.js +96 -0
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/dev/core/abr/ewma.js +71 -0
- package/dist/dev/core/abr/ewma.js.map +1 -0
- package/dist/dev/core/abr/quality-selection.js +53 -0
- package/dist/dev/core/abr/quality-selection.js.map +1 -0
- package/dist/dev/core/actor.d.ts +31 -0
- package/dist/dev/core/actor.d.ts.map +1 -0
- package/dist/dev/core/buffer/back-buffer.js +46 -0
- package/dist/dev/core/buffer/back-buffer.js.map +1 -0
- package/dist/dev/core/buffer/forward-buffer.js +73 -0
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
- package/dist/dev/core/events/create-event-stream.d.ts +48 -0
- package/dist/dev/core/events/create-event-stream.d.ts.map +1 -0
- package/dist/dev/core/events/create-event-stream.js +48 -0
- package/dist/dev/core/events/create-event-stream.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +51 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +68 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.d.ts +16 -0
- package/dist/dev/core/features/resolve-presentation.d.ts.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +99 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.d.ts +13 -0
- package/dist/dev/core/features/resolve-track.d.ts.map +1 -0
- package/dist/dev/core/features/resolve-track.js +77 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.d.ts +11 -0
- package/dist/dev/core/features/select-tracks.d.ts.map +1 -0
- package/dist/dev/core/features/select-tracks.js +147 -0
- package/dist/dev/core/features/select-tracks.js.map +1 -0
- package/dist/dev/core/hls/parse-attributes.js +134 -0
- package/dist/dev/core/hls/parse-attributes.js.map +1 -0
- package/dist/dev/core/hls/parse-media-playlist.js +83 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +215 -0
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
- package/dist/dev/core/hls/resolve-url.js +11 -0
- package/dist/dev/core/hls/resolve-url.js.map +1 -0
- package/dist/dev/core/reactive/combine-latest.js +72 -0
- package/dist/dev/core/reactive/combine-latest.js.map +1 -0
- package/dist/dev/core/state/create-state.d.ts +104 -0
- package/dist/dev/core/state/create-state.d.ts.map +1 -0
- package/dist/dev/core/state/create-state.js +143 -0
- package/dist/dev/core/state/create-state.js.map +1 -0
- package/dist/dev/core/task.js +122 -0
- package/dist/dev/core/task.js.map +1 -0
- package/dist/dev/core/types/index.d.ts +173 -0
- package/dist/dev/core/types/index.d.ts.map +1 -0
- package/dist/dev/core/types/index.js +15 -0
- package/dist/dev/core/types/index.js.map +1 -0
- package/dist/dev/core/utils/generate-id.js +21 -0
- package/dist/dev/core/utils/generate-id.js.map +1 -0
- package/dist/dev/core/utils/track-selection.d.ts +16 -0
- package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
- package/dist/dev/core/utils/track-selection.js +31 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +169 -0
- package/dist/dev/dom/features/end-of-stream.js.map +1 -0
- package/dist/dev/dom/features/load-segments.d.ts +72 -0
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
- package/dist/dev/dom/features/load-segments.js +188 -0
- package/dist/dev/dom/features/load-segments.js.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts +61 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.js +151 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +197 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +55 -0
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js +70 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/setup-text-tracks.js +88 -0
- package/dist/dev/dom/features/setup-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js +63 -0
- package/dist/dev/dom/features/sync-selected-text-track-from-dom.js.map +1 -0
- package/dist/dev/dom/features/sync-text-track-modes.js +38 -0
- package/dist/dev/dom/features/sync-text-track-modes.js.map +1 -0
- package/dist/dev/dom/features/track-current-time.d.ts +40 -0
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
- package/dist/dev/dom/features/track-current-time.js +46 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +53 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +55 -0
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.js +36 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +72 -0
- package/dist/dev/dom/features/update-duration.js.map +1 -0
- package/dist/dev/dom/media/append-segment.d.ts +24 -0
- package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
- package/dist/dev/dom/media/append-segment.js +60 -0
- package/dist/dev/dom/media/append-segment.js.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.js +55 -0
- package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
- package/dist/dev/dom/media/mediasource-setup.js +154 -0
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts +56 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.js +186 -0
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/dev/dom/network/fetch.js +56 -0
- package/dist/dev/dom/network/fetch.js.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts +41 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.js +95 -0
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
- package/dist/dev/dom/playback-engine/engine.d.ts +135 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +176 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +3 -0
- package/dist/dev/dom/playback-engine.js +4 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
- package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
- package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/dev/dom.d.ts +10 -0
- package/dist/dev/dom.js +11 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/package.json +22 -22
- package/dist/adapter-25AtMN8s.d.ts +0 -719
- package/dist/adapter-25AtMN8s.d.ts.map +0 -1
- package/dist/adapter-CflgYzjF.js +0 -3556
- package/dist/adapter-CflgYzjF.js.map +0 -1
- package/dist/dom/index.d.ts +0 -221
- package/dist/dom/index.d.ts.map +0 -1
- package/dist/dom/index.js.map +0 -1
- package/dist/dom/playback-engine/index.d.ts +0 -2
- package/dist/dom/playback-engine/index.js +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/src-DRsS_H80.js.map +0 -1
- /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,51 +1,27 @@
|
|
|
1
1
|
# @videojs/spf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[![package-badge]][package]
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
SPF is a lightweight, bundle-size-optimized HLS playback engine for Video.js 10.
|
|
8
|
-
|
|
9
|
-
**Target:** < 20KB (minified + gzipped)
|
|
10
|
-
**Target Date:** February 27, 2026
|
|
11
|
-
**Status:** Active development
|
|
12
|
-
|
|
13
|
-
## V1 Feature Set
|
|
14
|
-
|
|
15
|
-
- CMAF HLS VOD playback
|
|
16
|
-
- Basic ABR (EWMA throughput)
|
|
17
|
-
- WebVTT captions
|
|
18
|
-
- MSE + MMS support
|
|
19
|
-
- Chrome, Safari, Firefox, Edge (latest)
|
|
5
|
+
> **⚠️ Beta** Experimental adoption in real projects.
|
|
20
6
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# Install dependencies
|
|
25
|
-
pnpm install
|
|
26
|
-
|
|
27
|
-
# Run tests
|
|
28
|
-
pnpm test
|
|
29
|
-
|
|
30
|
-
# Build
|
|
31
|
-
pnpm build
|
|
7
|
+
## Overview
|
|
32
8
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
9
|
+
`@videojs/spf` is a lightweight, bundle-size-optimized streaming engine for Video.js 10. It provides
|
|
10
|
+
HLS playback with adaptive bitrate switching, WebVTT captions, and MSE support.
|
|
36
11
|
|
|
37
|
-
##
|
|
12
|
+
## Community
|
|
38
13
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- Architecture decisions
|
|
42
|
-
- Implementation timeline
|
|
14
|
+
If you need help with anything related to Video.js 10, or if you'd like to casually chat with other
|
|
15
|
+
members:
|
|
43
16
|
|
|
44
|
-
|
|
17
|
+
- [Join Discord Server][discord]
|
|
18
|
+
- [See GitHub Discussions][gh-discussions]
|
|
45
19
|
|
|
46
|
-
|
|
20
|
+
## License
|
|
47
21
|
|
|
48
|
-
|
|
22
|
+
[Apache-2.0](./LICENSE)
|
|
49
23
|
|
|
50
|
-
|
|
51
|
-
-
|
|
24
|
+
[package]: https://www.npmjs.com/package/@videojs/spf
|
|
25
|
+
[package-badge]: https://img.shields.io/npm/v/@videojs/spf?label=@videojs/spf
|
|
26
|
+
[discord]: https://discord.gg/JBqHh485uF
|
|
27
|
+
[gh-discussions]: https://github.com/videojs/v10/discussions
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { VERSION } from "./index.js";
|
|
2
|
+
import { createState } from "./core/state/create-state.js";
|
|
3
|
+
import { resolveUrl } from "./core/hls/resolve-url.js";
|
|
4
|
+
import { parseMediaPlaylist } from "./core/hls/parse-media-playlist.js";
|
|
5
|
+
import { parseMultivariantPlaylist } from "./core/hls/parse-multivariant.js";
|
|
6
|
+
import { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate, sampleBandwidth } from "./core/abr/bandwidth-estimator.js";
|
|
7
|
+
import { DEFAULT_QUALITY_CONFIG, selectQuality } from "./core/abr/quality-selection.js";
|
|
8
|
+
import { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint } from "./core/buffer/back-buffer.js";
|
|
9
|
+
import { DEFAULT_FORWARD_BUFFER_CONFIG, getSegmentsToLoad } from "./core/buffer/forward-buffer.js";
|
|
10
|
+
import { hasPresentationDuration, isResolvedTrack } from "./core/types/index.js";
|
|
11
|
+
import { attachMediaSource, createMediaSource, createSourceBuffer, isCodecSupported, supportsManagedMediaSource, supportsMediaSource, waitForSourceOpen } from "./dom/media/mediasource-setup.js";
|
|
12
|
+
import { fetchResolvable, getResponseText } from "./dom/network/fetch.js";
|
|
13
|
+
import { createEventStream } from "./core/events/create-event-stream.js";
|
|
14
|
+
import { combineLatest } from "./core/reactive/combine-latest.js";
|
|
15
|
+
import { canResolve, isUnresolved, resolvePresentation, shouldResolve, syncPreloadAttribute } from "./core/features/resolve-presentation.js";
|
|
16
|
+
import { DEFAULT_SWITCHING_CONFIG, switchQuality } from "./core/features/quality-switching.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { applyZeroFactor, calculateEwma } from "./ewma.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/abr/bandwidth-estimator.ts
|
|
4
|
+
/**
|
|
5
|
+
* Dual EWMA Bandwidth Estimator
|
|
6
|
+
*
|
|
7
|
+
* Estimates available bandwidth using two EWMA calculations with different
|
|
8
|
+
* half-lives, taking the minimum of both. This approach (from Shaka Player):
|
|
9
|
+
*
|
|
10
|
+
* - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
|
|
11
|
+
* - **Slow EWMA** (5s half-life): Provides stability during fluctuations
|
|
12
|
+
* - **min(fast, slow)**: Adapts down quickly, up slowly
|
|
13
|
+
*
|
|
14
|
+
* This naturally provides asymmetric behavior needed for good QoE:
|
|
15
|
+
* avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Default bandwidth estimator configuration.
|
|
19
|
+
*
|
|
20
|
+
* Values match Shaka Player defaults based on experimentation.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_BANDWIDTH_CONFIG = {
|
|
23
|
+
fastHalfLife: 2,
|
|
24
|
+
slowHalfLife: 5,
|
|
25
|
+
minTotalBytes: 128e3,
|
|
26
|
+
minBytes: 16e3,
|
|
27
|
+
minDuration: 5
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Add a bandwidth sample from a segment download.
|
|
31
|
+
*
|
|
32
|
+
* Samples are filtered based on:
|
|
33
|
+
* - Minimum bytes (filters TTFB-dominated small segments)
|
|
34
|
+
* - Minimum duration (filters cached responses)
|
|
35
|
+
*
|
|
36
|
+
* Valid samples update both fast and slow EWMA estimates.
|
|
37
|
+
*
|
|
38
|
+
* @param state - Current estimator state
|
|
39
|
+
* @param durationMs - Download duration in milliseconds
|
|
40
|
+
* @param numBytes - Number of bytes downloaded
|
|
41
|
+
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
42
|
+
* @returns New estimator state with sample incorporated (or unchanged if filtered)
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* let state = { fastEstimate: 0, fastTotalWeight: 0, ... };
|
|
46
|
+
* // Sample: 1MB in 1 second
|
|
47
|
+
* state = sampleBandwidth(state, 1000, 1_000_000);
|
|
48
|
+
*/
|
|
49
|
+
function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
50
|
+
const updatedBytesSampled = state.bytesSampled + numBytes;
|
|
51
|
+
if (numBytes < config.minBytes) return {
|
|
52
|
+
...state,
|
|
53
|
+
bytesSampled: updatedBytesSampled
|
|
54
|
+
};
|
|
55
|
+
if (durationMs < config.minDuration) return {
|
|
56
|
+
...state,
|
|
57
|
+
bytesSampled: updatedBytesSampled
|
|
58
|
+
};
|
|
59
|
+
const bandwidth = 8e3 * numBytes / durationMs;
|
|
60
|
+
const weight = durationMs / 1e3;
|
|
61
|
+
return {
|
|
62
|
+
fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),
|
|
63
|
+
fastTotalWeight: state.fastTotalWeight + weight,
|
|
64
|
+
slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),
|
|
65
|
+
slowTotalWeight: state.slowTotalWeight + weight,
|
|
66
|
+
bytesSampled: updatedBytesSampled
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the current bandwidth estimate.
|
|
71
|
+
*
|
|
72
|
+
* Returns the **minimum** of the fast and slow EWMA estimates.
|
|
73
|
+
* This provides the key asymmetric behavior:
|
|
74
|
+
* - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
|
|
75
|
+
* - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
|
|
76
|
+
*
|
|
77
|
+
* Uses default estimate until enough data has been sampled.
|
|
78
|
+
*
|
|
79
|
+
* @param state - Current estimator state
|
|
80
|
+
* @param defaultEstimate - Fallback estimate before sufficient samples (bps)
|
|
81
|
+
* @param config - Optional estimator configuration (uses defaults if not provided)
|
|
82
|
+
* @returns Bandwidth estimate in bits per second
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
|
|
86
|
+
*/
|
|
87
|
+
function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
|
|
88
|
+
if (state.bytesSampled < config.minTotalBytes) return defaultEstimate;
|
|
89
|
+
const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
|
|
90
|
+
const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
|
|
91
|
+
return Math.min(fastEstimate, slowEstimate);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
export { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate, sampleBandwidth };
|
|
96
|
+
//# sourceMappingURL=bandwidth-estimator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bandwidth-estimator.js","names":[],"sources":["../../../../src/core/abr/bandwidth-estimator.ts"],"sourcesContent":["/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled.\n *\n * @param state - Current estimator state\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,gBACd,OACA,YACA,UACA,SAA0B,0BACV;CAEhB,MAAM,sBAAsB,MAAM,eAAe;AAKjD,KAAI,WAAW,OAAO,SACpB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;AAMxD,KAAI,aAAa,OAAO,YACtB,QAAO;EAAE,GAAG;EAAO,cAAc;EAAqB;CAKxD,MAAM,YAAa,MAAO,WAAY;CAItC,MAAM,SAAS,aAAa;AAE5B,QAAO;EACL,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,aAAa;EACvF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc;EACf;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,qBACd,OACA,iBACA,SAA0B,0BAClB;AAER,KAAI,MAAM,eAAe,OAAO,cAC9B,QAAO;CAIT,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;CAEpG,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,aAAa;AAIpG,QAAO,KAAK,IAAI,cAAc,aAAa"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/core/abr/ewma.ts
|
|
2
|
+
/**
|
|
3
|
+
* Exponentially Weighted Moving Average (EWMA)
|
|
4
|
+
*
|
|
5
|
+
* Pure functional implementation of EWMA calculations.
|
|
6
|
+
* Based on Shaka Player's EWMA algorithm.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Calculate alpha (decay factor) from half-life.
|
|
10
|
+
*
|
|
11
|
+
* Alpha determines how quickly old data "expires":
|
|
12
|
+
* - alpha close to 1 = slow decay (long memory)
|
|
13
|
+
* - alpha close to 0 = fast decay (short memory)
|
|
14
|
+
*
|
|
15
|
+
* @param halfLife - The quantity of prior samples (by weight) that make up
|
|
16
|
+
* half of the new estimate. Must be positive.
|
|
17
|
+
* @returns Alpha value between 0 and 1
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life
|
|
21
|
+
*/
|
|
22
|
+
function calculateAlpha(halfLife) {
|
|
23
|
+
return Math.exp(Math.log(.5) / halfLife);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Calculate exponentially weighted moving average.
|
|
27
|
+
*
|
|
28
|
+
* Updates an estimate by blending a new value with the previous estimate,
|
|
29
|
+
* weighted by the sample duration. Longer samples have more influence.
|
|
30
|
+
*
|
|
31
|
+
* @param prevEstimate - Previous EWMA estimate
|
|
32
|
+
* @param value - New sample value to incorporate
|
|
33
|
+
* @param weight - Sample weight (typically duration in seconds)
|
|
34
|
+
* @param halfLife - Half-life for decay (typically 2-5 seconds)
|
|
35
|
+
* @returns Updated EWMA estimate
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* let estimate = 0;
|
|
39
|
+
* estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample
|
|
40
|
+
* estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample
|
|
41
|
+
*/
|
|
42
|
+
function calculateEwma(prevEstimate, value, weight, halfLife) {
|
|
43
|
+
const adjAlpha = calculateAlpha(halfLife) ** weight;
|
|
44
|
+
return value * (1 - adjAlpha) + adjAlpha * prevEstimate;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Apply zero-factor correction to EWMA estimate.
|
|
48
|
+
*
|
|
49
|
+
* The zero-factor correction compensates for bias when starting from zero.
|
|
50
|
+
* Without this correction, early estimates would be artificially low.
|
|
51
|
+
*
|
|
52
|
+
* As totalWeight increases, the correction factor approaches 1, meaning
|
|
53
|
+
* the estimate becomes more reliable and needs less correction.
|
|
54
|
+
*
|
|
55
|
+
* @param estimate - Raw EWMA estimate (uncorrected)
|
|
56
|
+
* @param totalWeight - Accumulated weight from all samples
|
|
57
|
+
* @param halfLife - Half-life used in EWMA calculation
|
|
58
|
+
* @returns Corrected estimate, or 0 if totalWeight is 0
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const raw = calculateEwma(0, 1_000_000, 1, 2);
|
|
62
|
+
* const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000
|
|
63
|
+
*/
|
|
64
|
+
function applyZeroFactor(estimate, totalWeight, halfLife) {
|
|
65
|
+
if (totalWeight === 0) return 0;
|
|
66
|
+
return estimate / (1 - calculateAlpha(halfLife) ** totalWeight);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { applyZeroFactor, calculateEwma };
|
|
71
|
+
//# sourceMappingURL=ewma.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ewma.js","names":[],"sources":["../../../../src/core/abr/ewma.ts"],"sourcesContent":["/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,UAA0B;AACvD,QAAO,KAAK,IAAI,KAAK,IAAI,GAAI,GAAG,SAAS;;;;;;;;;;;;;;;;;;;AAoB3C,SAAgB,cAAc,cAAsB,OAAe,QAAgB,UAA0B;CAE3G,MAAM,WADQ,eAAe,SAAS,IACZ;AAC1B,QAAO,SAAS,IAAI,YAAY,WAAW;;;;;;;;;;;;;;;;;;;;AAqB7C,SAAgB,gBAAgB,UAAkB,aAAqB,UAA0B;AAC/F,KAAI,gBAAgB,EAClB,QAAO;AAKT,QAAO,YADY,IADL,eAAe,SAAS,IACN"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/core/abr/quality-selection.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default quality selection configuration.
|
|
4
|
+
* Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
|
|
7
|
+
/**
|
|
8
|
+
* Select the best video track based on current bandwidth estimate.
|
|
9
|
+
*
|
|
10
|
+
* Selects the highest quality track where bandwidth is sufficient with safety margin:
|
|
11
|
+
* - currentBandwidth >= track.bandwidth / safetyMargin
|
|
12
|
+
* - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
|
|
13
|
+
* - At same bandwidth, prefers higher resolution
|
|
14
|
+
*
|
|
15
|
+
* @param tracks - Available video tracks (can be unsorted)
|
|
16
|
+
* @param currentBandwidth - Current bandwidth estimate in bits per second
|
|
17
|
+
* @param config - Optional quality selection configuration
|
|
18
|
+
* @returns Selected track, or undefined if no tracks available
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const tracks = [
|
|
22
|
+
* { id: '360p', bandwidth: 500_000, ... },
|
|
23
|
+
* { id: '720p', bandwidth: 2_000_000, ... },
|
|
24
|
+
* { id: '1080p', bandwidth: 4_000_000, ... },
|
|
25
|
+
* ];
|
|
26
|
+
*
|
|
27
|
+
* // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
|
|
28
|
+
* const selected = selectQuality(tracks, 2_500_000);
|
|
29
|
+
*/
|
|
30
|
+
function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
|
|
31
|
+
if (tracks.length === 0) return;
|
|
32
|
+
const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
|
|
33
|
+
let chosen;
|
|
34
|
+
for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
|
|
35
|
+
if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
|
|
36
|
+
}
|
|
37
|
+
return chosen ?? sortedTracks[0];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if track A has higher resolution than track B.
|
|
41
|
+
* Compares by total pixel count (width × height).
|
|
42
|
+
*
|
|
43
|
+
* @param trackA - First track to compare
|
|
44
|
+
* @param trackB - Second track to compare
|
|
45
|
+
* @returns True if trackA has more pixels than trackB
|
|
46
|
+
*/
|
|
47
|
+
function hasHigherResolution(trackA, trackB) {
|
|
48
|
+
return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { DEFAULT_QUALITY_CONFIG, selectQuality };
|
|
53
|
+
//# sourceMappingURL=quality-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality-selection.js","names":[],"sources":["../../../../src/core/abr/quality-selection.ts"],"sourcesContent":["/**\n * Quality Selection Algorithm\n *\n * Selects optimal video track based on bandwidth estimate with safety margin.\n * Stateless selection - picks highest quality that fits bandwidth.\n *\n * Key concepts:\n * - **Safety margin** (0.85): Pick track where bandwidth >= track.bandwidth / 0.85\n * - This ensures 15% headroom to avoid buffering\n * - At same bandwidth, prefer higher resolution\n */\n\nimport type { PartiallyResolvedVideoTrack } from '../types';\n\n/**\n * Quality selection configuration.\n */\nexport interface QualityConfig {\n /**\n * Safety margin (0-1).\n * To select a track, need: currentBandwidth >= track.bandwidth / safetyMargin.\n * Default 0.85 means track must use ≤85% of available bandwidth (15% headroom).\n */\n safetyMargin: number;\n}\n\n/**\n * Default quality selection configuration.\n * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).\n */\nexport const DEFAULT_QUALITY_CONFIG: QualityConfig = {\n safetyMargin: 0.85,\n};\n\n/**\n * Select the best video track based on current bandwidth estimate.\n *\n * Selects the highest quality track where bandwidth is sufficient with safety margin:\n * - currentBandwidth >= track.bandwidth / safetyMargin\n * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)\n * - At same bandwidth, prefers higher resolution\n *\n * @param tracks - Available video tracks (can be unsorted)\n * @param currentBandwidth - Current bandwidth estimate in bits per second\n * @param config - Optional quality selection configuration\n * @returns Selected track, or undefined if no tracks available\n *\n * @example\n * const tracks = [\n * { id: '360p', bandwidth: 500_000, ... },\n * { id: '720p', bandwidth: 2_000_000, ... },\n * { id: '1080p', bandwidth: 4_000_000, ... },\n * ];\n *\n * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)\n * const selected = selectQuality(tracks, 2_500_000);\n */\nexport function selectQuality(\n tracks: PartiallyResolvedVideoTrack[],\n currentBandwidth: number,\n config: QualityConfig = DEFAULT_QUALITY_CONFIG\n): PartiallyResolvedVideoTrack | undefined {\n if (tracks.length === 0) {\n return undefined;\n }\n\n // Sort tracks by bandwidth (lowest first)\n const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);\n\n // Start with no selection\n let chosen: PartiallyResolvedVideoTrack | undefined;\n\n for (const track of sortedTracks) {\n // Check if we have enough bandwidth for this track with safety margin\n // Required bandwidth = track.bandwidth / safetyMargin\n const requiredBandwidth = track.bandwidth / config.safetyMargin;\n\n if (currentBandwidth >= requiredBandwidth) {\n // We can support this track - prefer it if better than current choice\n if (\n !chosen ||\n track.bandwidth > chosen.bandwidth ||\n (track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen))\n ) {\n chosen = track;\n }\n }\n }\n\n // If no track fits with safety margin, fall back to lowest quality\n return chosen ?? sortedTracks[0];\n}\n\n/**\n * Check if track A has higher resolution than track B.\n * Compares by total pixel count (width × height).\n *\n * @param trackA - First track to compare\n * @param trackB - Second track to compare\n * @returns True if trackA has more pixels than trackB\n */\nfunction hasHigherResolution(trackA: PartiallyResolvedVideoTrack, trackB: PartiallyResolvedVideoTrack): boolean {\n const pixelsA = (trackA.width ?? 0) * (trackA.height ?? 0);\n const pixelsB = (trackB.width ?? 0) * (trackB.height ?? 0);\n return pixelsA > pixelsB;\n}\n"],"mappings":";;;;;AA8BA,MAAa,yBAAwC,EACnD,cAAc,KACf;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,SAAgB,cACd,QACA,kBACA,SAAwB,wBACiB;AACzC,KAAI,OAAO,WAAW,EACpB;CAIF,MAAM,eAAe,OAAO,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;CAG7E,IAAI;AAEJ,MAAK,MAAM,SAAS,aAKlB,KAAI,oBAFsB,MAAM,YAAY,OAAO,cAIjD;MACE,CAAC,UACD,MAAM,YAAY,OAAO,aACxB,MAAM,cAAc,OAAO,aAAa,oBAAoB,OAAO,OAAO,CAE3E,UAAS;;AAMf,QAAO,UAAU,aAAa;;;;;;;;;;AAWhC,SAAS,oBAAoB,QAAqC,QAA8C;AAG9G,SAFiB,OAAO,SAAS,MAAM,OAAO,UAAU,MACvC,OAAO,SAAS,MAAM,OAAO,UAAU"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/core/buffer/back-buffer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default back buffer configuration.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
|
|
6
|
+
/**
|
|
7
|
+
* Calculate back buffer flush point.
|
|
8
|
+
*
|
|
9
|
+
* Determines where to flush old segments from the back buffer.
|
|
10
|
+
* Keeps a fixed number of segments behind the current playback position.
|
|
11
|
+
*
|
|
12
|
+
* Algorithm:
|
|
13
|
+
* 1. Find segments before currentTime
|
|
14
|
+
* 2. Count back N segments (keepSegments)
|
|
15
|
+
* 3. Return startTime of segment N+1 back (flush everything before this)
|
|
16
|
+
*
|
|
17
|
+
* @param segments - Available segments (should be sorted by startTime)
|
|
18
|
+
* @param currentTime - Current playback position in seconds
|
|
19
|
+
* @param config - Optional back buffer configuration
|
|
20
|
+
* @returns Time in seconds to flush up to (flush range: [0, flushEnd))
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const segments = [
|
|
24
|
+
* { startTime: 0, duration: 6, ... },
|
|
25
|
+
* { startTime: 6, duration: 6, ... },
|
|
26
|
+
* { startTime: 12, duration: 6, ... },
|
|
27
|
+
* { startTime: 18, duration: 6, ... },
|
|
28
|
+
* ];
|
|
29
|
+
*
|
|
30
|
+
* // Playing at 18s, keep 2 segments
|
|
31
|
+
* const flushEnd = calculateBackBufferFlushPoint(segments, 18);
|
|
32
|
+
* // Returns 6 (flush [0, 6), keep [6-18))
|
|
33
|
+
*/
|
|
34
|
+
function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
|
|
35
|
+
if (segments.length === 0) return 0;
|
|
36
|
+
const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
|
|
37
|
+
if (segmentsBefore.length === 0) return 0;
|
|
38
|
+
const segmentsToFlush = segmentsBefore.length - config.keepSegments;
|
|
39
|
+
if (segmentsToFlush <= 0) return 0;
|
|
40
|
+
if (segmentsToFlush >= segmentsBefore.length) return currentTime;
|
|
41
|
+
return segmentsBefore[segmentsToFlush].startTime;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint };
|
|
46
|
+
//# sourceMappingURL=back-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-buffer.js","names":[],"sources":["../../../../src/core/buffer/back-buffer.ts"],"sourcesContent":["/**\n * Back Buffer Strategy (Simple)\n *\n * Calculates flush points for back buffer management.\n * V1 uses simple \"keep N segments\" strategy.\n */\n\nimport type { Segment } from '../types';\n\n/**\n * Back buffer configuration.\n */\nexport interface BackBufferConfig {\n /**\n * Number of segments to keep behind current playback position.\n * Default: 2 segments.\n */\n keepSegments: number;\n}\n\n/**\n * Default back buffer configuration.\n */\nexport const DEFAULT_BACK_BUFFER_CONFIG: BackBufferConfig = {\n keepSegments: 2,\n};\n\n/**\n * Calculate back buffer flush point.\n *\n * Determines where to flush old segments from the back buffer.\n * Keeps a fixed number of segments behind the current playback position.\n *\n * Algorithm:\n * 1. Find segments before currentTime\n * 2. Count back N segments (keepSegments)\n * 3. Return startTime of segment N+1 back (flush everything before this)\n *\n * @param segments - Available segments (should be sorted by startTime)\n * @param currentTime - Current playback position in seconds\n * @param config - Optional back buffer configuration\n * @returns Time in seconds to flush up to (flush range: [0, flushEnd))\n *\n * @example\n * const segments = [\n * { startTime: 0, duration: 6, ... },\n * { startTime: 6, duration: 6, ... },\n * { startTime: 12, duration: 6, ... },\n * { startTime: 18, duration: 6, ... },\n * ];\n *\n * // Playing at 18s, keep 2 segments\n * const flushEnd = calculateBackBufferFlushPoint(segments, 18);\n * // Returns 6 (flush [0, 6), keep [6-18))\n */\nexport function calculateBackBufferFlushPoint(\n segments: Segment[],\n currentTime: number,\n config: BackBufferConfig = DEFAULT_BACK_BUFFER_CONFIG\n): number {\n if (segments.length === 0) {\n return 0;\n }\n\n // Find all segments before current time (not including current segment)\n const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);\n\n // If no segments before current time, nothing to flush\n if (segmentsBefore.length === 0) {\n return 0;\n }\n\n // Calculate how many segments to flush\n // Keep last N segments, flush the rest\n const segmentsToFlush = segmentsBefore.length - config.keepSegments;\n\n // If we don't have enough segments to flush, keep everything\n if (segmentsToFlush <= 0) {\n return 0;\n }\n\n // If we want to flush all segments, return currentTime\n // (flush everything before current playback position)\n if (segmentsToFlush >= segmentsBefore.length) {\n return currentTime;\n }\n\n // Return the startTime of the first segment we want to keep\n // Everything before this will be flushed\n return segmentsBefore[segmentsToFlush]!.startTime;\n}\n"],"mappings":";;;;AAuBA,MAAa,6BAA+C,EAC1D,cAAc,GACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BD,SAAgB,8BACd,UACA,aACA,SAA2B,4BACnB;AACR,KAAI,SAAS,WAAW,EACtB,QAAO;CAIT,MAAM,iBAAiB,SAAS,QAAQ,QAAQ,IAAI,YAAY,YAAY;AAG5E,KAAI,eAAe,WAAW,EAC5B,QAAO;CAKT,MAAM,kBAAkB,eAAe,SAAS,OAAO;AAGvD,KAAI,mBAAmB,EACrB,QAAO;AAKT,KAAI,mBAAmB,eAAe,OACpC,QAAO;AAKT,QAAO,eAAe,iBAAkB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/core/buffer/forward-buffer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default forward buffer configuration.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_FORWARD_BUFFER_CONFIG = { bufferDuration: 30 };
|
|
6
|
+
/**
|
|
7
|
+
* Get segments that need to be loaded for forward buffer.
|
|
8
|
+
*
|
|
9
|
+
* Determines which segments to load to maintain target buffer duration.
|
|
10
|
+
* Handles discontiguous buffering (gaps after seeks).
|
|
11
|
+
*
|
|
12
|
+
* Algorithm:
|
|
13
|
+
* 1. Calculate target time: currentTime + bufferDuration
|
|
14
|
+
* 2. Find all segments in range [currentTime, targetTime)
|
|
15
|
+
* 3. Filter out segments already buffered at that time position
|
|
16
|
+
* 4. Return segments to load (fills gaps + extends to target)
|
|
17
|
+
*
|
|
18
|
+
* @param segments - All available segments from playlist
|
|
19
|
+
* @param bufferedSegments - Segments already buffered (ordered by startTime)
|
|
20
|
+
* @param currentTime - Current playback position in seconds
|
|
21
|
+
* @param config - Optional forward buffer configuration
|
|
22
|
+
* @returns Array of segments to load (empty if buffer is sufficient)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // After seek: buffered [0-12, 18-30], playing at 7s
|
|
26
|
+
* const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });
|
|
27
|
+
* // Returns [seg-12, seg-30] (fills gap, extends to target 31s)
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Calculate the start time from which to flush forward buffer content.
|
|
31
|
+
*
|
|
32
|
+
* Content that starts at or beyond `currentTime + bufferDuration` is no
|
|
33
|
+
* longer needed for the current playback position and should be removed
|
|
34
|
+
* from the SourceBuffer. This prevents unbounded accumulation of scattered
|
|
35
|
+
* SourceBuffer content after seeks, which can cause QuotaExceededError on
|
|
36
|
+
* long-form content.
|
|
37
|
+
*
|
|
38
|
+
* Returns `Infinity` when nothing needs flushing (no buffered segments
|
|
39
|
+
* exist beyond the threshold).
|
|
40
|
+
*
|
|
41
|
+
* @param bufferedSegments - Segments currently tracked in the buffer model
|
|
42
|
+
* @param currentTime - Current playback position in seconds
|
|
43
|
+
* @param config - Optional forward buffer configuration
|
|
44
|
+
* @returns Start time to flush from (flush range: [flushStart, Infinity)),
|
|
45
|
+
* or Infinity if no flush is needed
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30
|
|
49
|
+
* const flushStart = calculateForwardFlushPoint(segments, 0);
|
|
50
|
+
* // Returns 30 — flush [30, Infinity), keep [0, 30)
|
|
51
|
+
*/
|
|
52
|
+
function calculateForwardFlushPoint(bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
|
|
53
|
+
if (bufferedSegments.length === 0) return Infinity;
|
|
54
|
+
const threshold = currentTime + config.bufferDuration;
|
|
55
|
+
const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);
|
|
56
|
+
if (beyond.length === 0) return Infinity;
|
|
57
|
+
return Math.min(...beyond.map((seg) => seg.startTime));
|
|
58
|
+
}
|
|
59
|
+
function getSegmentsToLoad(segments, bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
|
|
60
|
+
if (segments.length === 0) return [];
|
|
61
|
+
const targetTime = currentTime + config.bufferDuration;
|
|
62
|
+
const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));
|
|
63
|
+
return segments.filter((seg) => {
|
|
64
|
+
const segmentEnd = seg.startTime + seg.duration;
|
|
65
|
+
const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;
|
|
66
|
+
const isNotBuffered = !bufferedStartTimes.has(seg.startTime);
|
|
67
|
+
return isInRange && isNotBuffered;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { DEFAULT_FORWARD_BUFFER_CONFIG, calculateForwardFlushPoint, getSegmentsToLoad };
|
|
73
|
+
//# sourceMappingURL=forward-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forward-buffer.js","names":[],"sources":["../../../../src/core/buffer/forward-buffer.ts"],"sourcesContent":["/**\n * Forward Buffer Strategy (Simple)\n *\n * Determines which segments to load for forward buffer management.\n * V1 uses simple fixed-duration strategy (buffer N seconds ahead).\n */\n\nimport type { Segment } from '../types';\n\n/**\n * Forward buffer configuration.\n */\nexport interface ForwardBufferConfig {\n /**\n * Duration in seconds to buffer ahead of current playback position.\n * Default: 30 seconds.\n */\n bufferDuration: number;\n}\n\n/**\n * Default forward buffer configuration.\n */\nexport const DEFAULT_FORWARD_BUFFER_CONFIG: ForwardBufferConfig = {\n bufferDuration: 30,\n};\n\n/**\n * Get segments that need to be loaded for forward buffer.\n *\n * Determines which segments to load to maintain target buffer duration.\n * Handles discontiguous buffering (gaps after seeks).\n *\n * Algorithm:\n * 1. Calculate target time: currentTime + bufferDuration\n * 2. Find all segments in range [currentTime, targetTime)\n * 3. Filter out segments already buffered at that time position\n * 4. Return segments to load (fills gaps + extends to target)\n *\n * @param segments - All available segments from playlist\n * @param bufferedSegments - Segments already buffered (ordered by startTime)\n * @param currentTime - Current playback position in seconds\n * @param config - Optional forward buffer configuration\n * @returns Array of segments to load (empty if buffer is sufficient)\n *\n * @example\n * // After seek: buffered [0-12, 18-30], playing at 7s\n * const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });\n * // Returns [seg-12, seg-30] (fills gap, extends to target 31s)\n */\n/**\n * Calculate the start time from which to flush forward buffer content.\n *\n * Content that starts at or beyond `currentTime + bufferDuration` is no\n * longer needed for the current playback position and should be removed\n * from the SourceBuffer. This prevents unbounded accumulation of scattered\n * SourceBuffer content after seeks, which can cause QuotaExceededError on\n * long-form content.\n *\n * Returns `Infinity` when nothing needs flushing (no buffered segments\n * exist beyond the threshold).\n *\n * @param bufferedSegments - Segments currently tracked in the buffer model\n * @param currentTime - Current playback position in seconds\n * @param config - Optional forward buffer configuration\n * @returns Start time to flush from (flush range: [flushStart, Infinity)),\n * or Infinity if no flush is needed\n *\n * @example\n * // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30\n * const flushStart = calculateForwardFlushPoint(segments, 0);\n * // Returns 30 — flush [30, Infinity), keep [0, 30)\n */\nexport function calculateForwardFlushPoint(\n bufferedSegments: readonly Segment[],\n currentTime: number,\n config: ForwardBufferConfig = DEFAULT_FORWARD_BUFFER_CONFIG\n): number {\n if (bufferedSegments.length === 0) return Infinity;\n\n const threshold = currentTime + config.bufferDuration;\n\n // Find segments that start at or beyond the threshold\n const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);\n\n if (beyond.length === 0) return Infinity;\n\n // Flush from the earliest such segment onward\n return Math.min(...beyond.map((seg) => seg.startTime));\n}\n\nexport function getSegmentsToLoad(\n segments: readonly Segment[],\n bufferedSegments: readonly Segment[],\n currentTime: number,\n config: ForwardBufferConfig = DEFAULT_FORWARD_BUFFER_CONFIG\n): Segment[] {\n if (segments.length === 0) {\n return [];\n }\n\n // Calculate target buffer end time\n const targetTime = currentTime + config.bufferDuration;\n\n // Create set of buffered segment start times for fast lookup\n // V1 simple: if ANY segment is buffered at a given time, don't load for that time\n // V2 (future): would compare by startTime + bitrate/track for quality switching\n const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));\n\n // Find segments to load:\n // - Overlaps buffer window [currentTime, targetTime)\n // - Not already buffered at that time position\n const toLoad = segments.filter((seg) => {\n // Segment must overlap the buffer window\n const segmentEnd = seg.startTime + seg.duration;\n const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;\n\n // Must not have a segment buffered at this time position\n const isNotBuffered = !bufferedStartTimes.has(seg.startTime);\n\n return isInRange && isNotBuffered;\n });\n\n return toLoad;\n}\n"],"mappings":";;;;AAuBA,MAAa,gCAAqD,EAChE,gBAAgB,IACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDD,SAAgB,2BACd,kBACA,aACA,SAA8B,+BACtB;AACR,KAAI,iBAAiB,WAAW,EAAG,QAAO;CAE1C,MAAM,YAAY,cAAc,OAAO;CAGvC,MAAM,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,aAAa,UAAU;AAE3E,KAAI,OAAO,WAAW,EAAG,QAAO;AAGhC,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,QAAQ,IAAI,UAAU,CAAC;;AAGxD,SAAgB,kBACd,UACA,kBACA,aACA,SAA8B,+BACnB;AACX,KAAI,SAAS,WAAW,EACtB,QAAO,EAAE;CAIX,MAAM,aAAa,cAAc,OAAO;CAKxC,MAAM,qBAAqB,IAAI,IAAI,iBAAiB,KAAK,QAAQ,IAAI,UAAU,CAAC;AAgBhF,QAXe,SAAS,QAAQ,QAAQ;EAEtC,MAAM,aAAa,IAAI,YAAY,IAAI;EACvC,MAAM,YAAY,IAAI,YAAY,cAAc,aAAa;EAG7D,MAAM,gBAAgB,CAAC,mBAAmB,IAAI,IAAI,UAAU;AAE5D,SAAO,aAAa;GACpB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/core/events/create-event-stream.ts
|
|
2
|
+
/**
|
|
3
|
+
* Minimal event stream with Observable-like shape.
|
|
4
|
+
*
|
|
5
|
+
* Simple Subject/Observable-like implementation for dispatching discrete events.
|
|
6
|
+
* Events are dispatched synchronously to all subscribers.
|
|
7
|
+
*/
|
|
8
|
+
const EVENT_STREAM_SYMBOL = Symbol("@videojs/event-stream");
|
|
9
|
+
/**
|
|
10
|
+
* Creates a minimal event stream for dispatching discrete events.
|
|
11
|
+
*
|
|
12
|
+
* Events are dispatched synchronously to all subscribers.
|
|
13
|
+
* Conforms to Observable-like shape for future compatibility.
|
|
14
|
+
*
|
|
15
|
+
* Events must have a 'type' property for discriminated union type narrowing.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* type Action = { type: 'PLAY' } | { type: 'PAUSE' };
|
|
20
|
+
* const events = createEventStream<Action>();
|
|
21
|
+
*
|
|
22
|
+
* events.subscribe((action) => {
|
|
23
|
+
* if (action.type === 'PLAY') {
|
|
24
|
+
* // Type narrowed to { type: 'PLAY' }
|
|
25
|
+
* }
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* events.dispatch({ type: 'PLAY' });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
function createEventStream() {
|
|
32
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
33
|
+
return {
|
|
34
|
+
[EVENT_STREAM_SYMBOL]: true,
|
|
35
|
+
dispatch(event) {
|
|
36
|
+
const current = Array.from(subscribers);
|
|
37
|
+
for (const listener of current) listener(event);
|
|
38
|
+
},
|
|
39
|
+
subscribe(listener) {
|
|
40
|
+
subscribers.add(listener);
|
|
41
|
+
return () => subscribers.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { createEventStream };
|
|
48
|
+
//# sourceMappingURL=create-event-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-event-stream.js","names":[],"sources":["../../../../src/core/events/create-event-stream.ts"],"sourcesContent":["/**\n * Minimal event stream with Observable-like shape.\n *\n * Simple Subject/Observable-like implementation for dispatching discrete events.\n * Events are dispatched synchronously to all subscribers.\n */\n\nconst EVENT_STREAM_SYMBOL = Symbol('@videojs/event-stream');\n\n/**\n * Event listener function.\n */\nexport type EventListener<T extends Pick<Event, 'type'>> = (event: T) => void;\n\n/**\n * Event stream interface.\n */\nexport interface EventStream<T extends Pick<Event, 'type'> = { type: string }> {\n [EVENT_STREAM_SYMBOL]: true;\n dispatch(event: T): void;\n subscribe(listener: EventListener<T>): () => void;\n}\n\n/**\n * Creates a minimal event stream for dispatching discrete events.\n *\n * Events are dispatched synchronously to all subscribers.\n * Conforms to Observable-like shape for future compatibility.\n *\n * Events must have a 'type' property for discriminated union type narrowing.\n *\n * @example\n * ```ts\n * type Action = { type: 'PLAY' } | { type: 'PAUSE' };\n * const events = createEventStream<Action>();\n *\n * events.subscribe((action) => {\n * if (action.type === 'PLAY') {\n * // Type narrowed to { type: 'PLAY' }\n * }\n * });\n *\n * events.dispatch({ type: 'PLAY' });\n * ```\n */\nexport function createEventStream<T extends Pick<Event, 'type'>>(): EventStream<T> {\n const subscribers = new Set<EventListener<T>>();\n\n return {\n [EVENT_STREAM_SYMBOL]: true,\n\n dispatch(event: T): void {\n // Snapshot subscribers to avoid notifying those added during dispatch\n const current = Array.from(subscribers);\n for (const listener of current) {\n listener(event);\n }\n },\n\n subscribe(listener: EventListener<T>): () => void {\n subscribers.add(listener);\n return () => subscribers.delete(listener);\n },\n };\n}\n\n/**\n * Type guard to check if value is an EventStream.\n */\nexport function isEventStream(value: unknown): value is EventStream {\n return typeof value === 'object' && value !== null && EVENT_STREAM_SYMBOL in value;\n}\n"],"mappings":";;;;;;;AAOA,MAAM,sBAAsB,OAAO,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;AAsC3D,SAAgB,oBAAmE;CACjF,MAAM,8BAAc,IAAI,KAAuB;AAE/C,QAAO;GACJ,sBAAsB;EAEvB,SAAS,OAAgB;GAEvB,MAAM,UAAU,MAAM,KAAK,YAAY;AACvC,QAAK,MAAM,YAAY,QACrB,UAAS,MAAM;;EAInB,UAAU,UAAwC;AAChD,eAAY,IAAI,SAAS;AACzB,gBAAa,YAAY,OAAO,SAAS;;EAE5C"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isResolvedTrack } from "../types/index.js";
|
|
2
|
+
import { combineLatest } from "../reactive/combine-latest.js";
|
|
3
|
+
import { getSelectedTrack } from "../utils/track-selection.js";
|
|
4
|
+
|
|
5
|
+
//#region src/core/features/calculate-presentation-duration.ts
|
|
6
|
+
/**
|
|
7
|
+
* Check if we can calculate presentation duration (have required data).
|
|
8
|
+
*/
|
|
9
|
+
function canCalculateDuration(state) {
|
|
10
|
+
if (!state.presentation) return false;
|
|
11
|
+
return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check if we should calculate presentation duration (conditions met).
|
|
15
|
+
*/
|
|
16
|
+
function shouldCalculateDuration(state) {
|
|
17
|
+
if (!canCalculateDuration(state)) return false;
|
|
18
|
+
const { presentation } = state;
|
|
19
|
+
if (presentation.duration !== void 0) return false;
|
|
20
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
21
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
22
|
+
return !!(videoTrack && isResolvedTrack(videoTrack) || audioTrack && isResolvedTrack(audioTrack));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get duration from the first resolved track (prefer video, fallback to audio).
|
|
26
|
+
*/
|
|
27
|
+
function getDurationFromResolvedTracks(state) {
|
|
28
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
29
|
+
if (videoTrack && isResolvedTrack(videoTrack)) return videoTrack.duration;
|
|
30
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
31
|
+
if (audioTrack && isResolvedTrack(audioTrack)) return audioTrack.duration;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Calculate and set presentation duration from resolved tracks.
|
|
35
|
+
*/
|
|
36
|
+
function calculatePresentationDuration({ state }) {
|
|
37
|
+
return combineLatest([state]).subscribe(([currentState]) => {
|
|
38
|
+
if (!shouldCalculateDuration(currentState)) return;
|
|
39
|
+
const duration = getDurationFromResolvedTracks(currentState);
|
|
40
|
+
if (duration === void 0 || !Number.isFinite(duration)) return;
|
|
41
|
+
const { presentation } = currentState;
|
|
42
|
+
state.patch({ presentation: {
|
|
43
|
+
...presentation,
|
|
44
|
+
duration
|
|
45
|
+
} });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { calculatePresentationDuration };
|
|
51
|
+
//# sourceMappingURL=calculate-presentation-duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculate-presentation-duration.js","names":[],"sources":["../../../../src/core/features/calculate-presentation-duration.ts"],"sourcesContent":["import { combineLatest } from '../reactive/combine-latest';\nimport type { WritableState } from '../state/create-state';\nimport type { AudioTrack, Presentation, VideoTrack } from '../types';\nimport { isResolvedTrack } from '../types';\nimport { getSelectedTrack } from '../utils/track-selection';\n\nexport interface PresentationDurationState {\n presentation?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n}\n\n/**\n * Check if we can calculate presentation duration (have required data).\n */\nexport function canCalculateDuration(state: PresentationDurationState): boolean {\n if (!state.presentation) return false;\n // Need at least one selected track\n return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);\n}\n\n/**\n * Check if we should calculate presentation duration (conditions met).\n */\nexport function shouldCalculateDuration(state: PresentationDurationState): boolean {\n if (!canCalculateDuration(state)) return false;\n\n const { presentation } = state;\n\n // Don't recalculate if already set\n if (presentation!.duration !== undefined) return false;\n\n // Check if any selected track is resolved\n const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, 'video') : undefined;\n const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, 'audio') : undefined;\n\n // At least one track must be resolved (has segments and duration)\n return !!((videoTrack && isResolvedTrack(videoTrack)) || (audioTrack && isResolvedTrack(audioTrack)));\n}\n\n/**\n * Get duration from the first resolved track (prefer video, fallback to audio).\n */\nexport function getDurationFromResolvedTracks(state: PresentationDurationState): number | undefined {\n // Try video track first\n const videoTrack = state.selectedVideoTrackId\n ? (getSelectedTrack(state, 'video') as VideoTrack | undefined)\n : undefined;\n if (videoTrack && isResolvedTrack(videoTrack)) {\n return videoTrack.duration;\n }\n\n // Fallback to audio track\n const audioTrack = state.selectedAudioTrackId\n ? (getSelectedTrack(state, 'audio') as AudioTrack | undefined)\n : undefined;\n if (audioTrack && isResolvedTrack(audioTrack)) {\n return audioTrack.duration;\n }\n\n return undefined;\n}\n\n/**\n * Calculate and set presentation duration from resolved tracks.\n */\nexport function calculatePresentationDuration({\n state,\n}: {\n state: WritableState<PresentationDurationState>;\n}): () => void {\n return combineLatest([state]).subscribe(([currentState]: [PresentationDurationState]) => {\n if (!shouldCalculateDuration(currentState)) return;\n\n const duration = getDurationFromResolvedTracks(currentState);\n if (duration === undefined || !Number.isFinite(duration)) return;\n\n const { presentation } = currentState;\n\n // Patch presentation with duration\n state.patch({\n presentation: {\n ...presentation!,\n duration,\n },\n });\n });\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,qBAAqB,OAA2C;AAC9E,KAAI,CAAC,MAAM,aAAc,QAAO;AAEhC,QAAO,CAAC,EAAE,MAAM,wBAAwB,MAAM;;;;;AAMhD,SAAgB,wBAAwB,OAA2C;AACjF,KAAI,CAAC,qBAAqB,MAAM,CAAE,QAAO;CAEzC,MAAM,EAAE,iBAAiB;AAGzB,KAAI,aAAc,aAAa,OAAW,QAAO;CAGjD,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG;CACnF,MAAM,aAAa,MAAM,uBAAuB,iBAAiB,OAAO,QAAQ,GAAG;AAGnF,QAAO,CAAC,EAAG,cAAc,gBAAgB,WAAW,IAAM,cAAc,gBAAgB,WAAW;;;;;AAMrG,SAAgB,8BAA8B,OAAsD;CAElG,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;CAIpB,MAAM,aAAa,MAAM,uBACpB,iBAAiB,OAAO,QAAQ,GACjC;AACJ,KAAI,cAAc,gBAAgB,WAAW,CAC3C,QAAO,WAAW;;;;;AAStB,SAAgB,8BAA8B,EAC5C,SAGa;AACb,QAAO,cAAc,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,kBAA+C;AACvF,MAAI,CAAC,wBAAwB,aAAa,CAAE;EAE5C,MAAM,WAAW,8BAA8B,aAAa;AAC5D,MAAI,aAAa,UAAa,CAAC,OAAO,SAAS,SAAS,CAAE;EAE1D,MAAM,EAAE,iBAAiB;AAGzB,QAAM,MAAM,EACV,cAAc;GACZ,GAAG;GACH;GACD,EACF,CAAC;GACF"}
|