@livepeer-frameworks/player-core 0.1.1 → 0.1.2
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/dist/cjs/core/ABRController.js +456 -0
- package/dist/cjs/core/ABRController.js.map +1 -0
- package/dist/cjs/core/CodecUtils.js +195 -0
- package/dist/cjs/core/CodecUtils.js.map +1 -0
- package/dist/cjs/core/ErrorClassifier.js +410 -0
- package/dist/cjs/core/ErrorClassifier.js.map +1 -0
- package/dist/cjs/core/EventEmitter.js +108 -0
- package/dist/cjs/core/EventEmitter.js.map +1 -0
- package/dist/cjs/core/GatewayClient.js +342 -0
- package/dist/cjs/core/GatewayClient.js.map +1 -0
- package/dist/cjs/core/InteractionController.js +606 -0
- package/dist/cjs/core/InteractionController.js.map +1 -0
- package/dist/cjs/core/LiveDurationProxy.js +186 -0
- package/dist/cjs/core/LiveDurationProxy.js.map +1 -0
- package/dist/cjs/core/MetaTrackManager.js +624 -0
- package/dist/cjs/core/MetaTrackManager.js.map +1 -0
- package/dist/cjs/core/MistReporter.js +449 -0
- package/dist/cjs/core/MistReporter.js.map +1 -0
- package/dist/cjs/core/MistSignaling.js +264 -0
- package/dist/cjs/core/MistSignaling.js.map +1 -0
- package/dist/cjs/core/PlayerController.js +2658 -0
- package/dist/cjs/core/PlayerController.js.map +1 -0
- package/dist/cjs/core/PlayerInterface.js +269 -0
- package/dist/cjs/core/PlayerInterface.js.map +1 -0
- package/dist/cjs/core/PlayerManager.js +806 -0
- package/dist/cjs/core/PlayerManager.js.map +1 -0
- package/dist/cjs/core/PlayerRegistry.js +270 -0
- package/dist/cjs/core/PlayerRegistry.js.map +1 -0
- package/dist/cjs/core/QualityMonitor.js +474 -0
- package/dist/cjs/core/QualityMonitor.js.map +1 -0
- package/dist/cjs/core/SeekingUtils.js +292 -0
- package/dist/cjs/core/SeekingUtils.js.map +1 -0
- package/dist/cjs/core/StreamStateClient.js +381 -0
- package/dist/cjs/core/StreamStateClient.js.map +1 -0
- package/dist/cjs/core/SubtitleManager.js +227 -0
- package/dist/cjs/core/SubtitleManager.js.map +1 -0
- package/dist/cjs/core/TelemetryReporter.js +258 -0
- package/dist/cjs/core/TelemetryReporter.js.map +1 -0
- package/dist/cjs/core/TimeFormat.js +176 -0
- package/dist/cjs/core/TimeFormat.js.map +1 -0
- package/dist/cjs/core/TimerManager.js +176 -0
- package/dist/cjs/core/TimerManager.js.map +1 -0
- package/dist/cjs/core/UrlUtils.js +160 -0
- package/dist/cjs/core/UrlUtils.js.map +1 -0
- package/dist/cjs/core/detector.js +293 -0
- package/dist/cjs/core/detector.js.map +1 -0
- package/dist/cjs/core/scorer.js +443 -0
- package/dist/cjs/core/scorer.js.map +1 -0
- package/dist/cjs/index.js +121 -20134
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/utils.js +11 -0
- package/dist/cjs/lib/utils.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +6 -0
- package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3042 -0
- package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/cjs/players/DashJsPlayer.js +638 -0
- package/dist/cjs/players/DashJsPlayer.js.map +1 -0
- package/dist/cjs/players/HlsJsPlayer.js +482 -0
- package/dist/cjs/players/HlsJsPlayer.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js +522 -0
- package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js +215 -0
- package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/index.js +987 -0
- package/dist/cjs/players/MewsWsPlayer/index.js.map +1 -0
- package/dist/cjs/players/MistPlayer.js +185 -0
- package/dist/cjs/players/MistPlayer.js.map +1 -0
- package/dist/cjs/players/MistWebRTCPlayer/index.js +635 -0
- package/dist/cjs/players/MistWebRTCPlayer/index.js.map +1 -0
- package/dist/cjs/players/NativePlayer.js +762 -0
- package/dist/cjs/players/NativePlayer.js.map +1 -0
- package/dist/cjs/players/VideoJsPlayer.js +585 -0
- package/dist/cjs/players/VideoJsPlayer.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js +236 -0
- package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js +143 -0
- package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js +96 -0
- package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/SyncController.js +359 -0
- package/dist/cjs/players/WebCodecsPlayer/SyncController.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js +460 -0
- package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/index.js +1467 -0
- package/dist/cjs/players/WebCodecsPlayer/index.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +320 -0
- package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
- package/dist/cjs/styles/index.js +57 -0
- package/dist/cjs/styles/index.js.map +1 -0
- package/dist/cjs/vanilla/FrameWorksPlayer.js +269 -0
- package/dist/cjs/vanilla/FrameWorksPlayer.js.map +1 -0
- package/dist/cjs/vanilla.js +11 -0
- package/dist/cjs/vanilla.js.map +1 -0
- package/dist/esm/core/ABRController.js +454 -0
- package/dist/esm/core/ABRController.js.map +1 -0
- package/dist/esm/core/CodecUtils.js +193 -0
- package/dist/esm/core/CodecUtils.js.map +1 -0
- package/dist/esm/core/ErrorClassifier.js +408 -0
- package/dist/esm/core/ErrorClassifier.js.map +1 -0
- package/dist/esm/core/EventEmitter.js +106 -0
- package/dist/esm/core/EventEmitter.js.map +1 -0
- package/dist/esm/core/GatewayClient.js +340 -0
- package/dist/esm/core/GatewayClient.js.map +1 -0
- package/dist/esm/core/InteractionController.js +604 -0
- package/dist/esm/core/InteractionController.js.map +1 -0
- package/dist/esm/core/LiveDurationProxy.js +184 -0
- package/dist/esm/core/LiveDurationProxy.js.map +1 -0
- package/dist/esm/core/MetaTrackManager.js +622 -0
- package/dist/esm/core/MetaTrackManager.js.map +1 -0
- package/dist/esm/core/MistReporter.js +447 -0
- package/dist/esm/core/MistReporter.js.map +1 -0
- package/dist/esm/core/MistSignaling.js +262 -0
- package/dist/esm/core/MistSignaling.js.map +1 -0
- package/dist/esm/core/PlayerController.js +2651 -0
- package/dist/esm/core/PlayerController.js.map +1 -0
- package/dist/esm/core/PlayerInterface.js +267 -0
- package/dist/esm/core/PlayerInterface.js.map +1 -0
- package/dist/esm/core/PlayerManager.js +804 -0
- package/dist/esm/core/PlayerManager.js.map +1 -0
- package/dist/esm/core/PlayerRegistry.js +264 -0
- package/dist/esm/core/PlayerRegistry.js.map +1 -0
- package/dist/esm/core/QualityMonitor.js +471 -0
- package/dist/esm/core/QualityMonitor.js.map +1 -0
- package/dist/esm/core/SeekingUtils.js +280 -0
- package/dist/esm/core/SeekingUtils.js.map +1 -0
- package/dist/esm/core/StreamStateClient.js +379 -0
- package/dist/esm/core/StreamStateClient.js.map +1 -0
- package/dist/esm/core/SubtitleManager.js +225 -0
- package/dist/esm/core/SubtitleManager.js.map +1 -0
- package/dist/esm/core/TelemetryReporter.js +256 -0
- package/dist/esm/core/TelemetryReporter.js.map +1 -0
- package/dist/esm/core/TimeFormat.js +169 -0
- package/dist/esm/core/TimeFormat.js.map +1 -0
- package/dist/esm/core/TimerManager.js +174 -0
- package/dist/esm/core/TimerManager.js.map +1 -0
- package/dist/esm/core/UrlUtils.js +151 -0
- package/dist/esm/core/UrlUtils.js.map +1 -0
- package/dist/esm/core/detector.js +279 -0
- package/dist/esm/core/detector.js.map +1 -0
- package/dist/esm/core/scorer.js +422 -0
- package/dist/esm/core/scorer.js.map +1 -0
- package/dist/esm/index.js +26 -20043
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/utils.js +9 -0
- package/dist/esm/lib/utils.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +4 -0
- package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3036 -0
- package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/esm/players/DashJsPlayer.js +636 -0
- package/dist/esm/players/DashJsPlayer.js.map +1 -0
- package/dist/esm/players/HlsJsPlayer.js +480 -0
- package/dist/esm/players/HlsJsPlayer.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js +520 -0
- package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/WebSocketManager.js +213 -0
- package/dist/esm/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/index.js +985 -0
- package/dist/esm/players/MewsWsPlayer/index.js.map +1 -0
- package/dist/esm/players/MistPlayer.js +183 -0
- package/dist/esm/players/MistPlayer.js.map +1 -0
- package/dist/esm/players/MistWebRTCPlayer/index.js +633 -0
- package/dist/esm/players/MistWebRTCPlayer/index.js.map +1 -0
- package/dist/esm/players/NativePlayer.js +759 -0
- package/dist/esm/players/NativePlayer.js.map +1 -0
- package/dist/esm/players/VideoJsPlayer.js +583 -0
- package/dist/esm/players/VideoJsPlayer.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js +233 -0
- package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js +134 -0
- package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js +91 -0
- package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/SyncController.js +357 -0
- package/dist/esm/players/WebCodecsPlayer/SyncController.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/WebSocketController.js +458 -0
- package/dist/esm/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/index.js +1458 -0
- package/dist/esm/players/WebCodecsPlayer/index.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +315 -0
- package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
- package/dist/esm/styles/index.js +54 -0
- package/dist/esm/styles/index.js.map +1 -0
- package/dist/esm/vanilla/FrameWorksPlayer.js +264 -0
- package/dist/esm/vanilla/FrameWorksPlayer.js.map +1 -0
- package/dist/esm/vanilla.js +2 -0
- package/dist/esm/vanilla.js.map +1 -0
- package/dist/player.css +4 -1
- package/dist/types/core/ABRController.d.ts +4 -4
- package/dist/types/core/CodecUtils.d.ts +1 -1
- package/dist/types/core/ErrorClassifier.d.ts +77 -0
- package/dist/types/core/GatewayClient.d.ts +4 -4
- package/dist/types/core/MetaTrackManager.d.ts +2 -2
- package/dist/types/core/MistReporter.d.ts +3 -3
- package/dist/types/core/MistSignaling.d.ts +12 -12
- package/dist/types/core/PlayerController.d.ts +19 -14
- package/dist/types/core/PlayerInterface.d.ts +100 -2
- package/dist/types/core/PlayerManager.d.ts +36 -9
- package/dist/types/core/PlayerRegistry.d.ts +11 -11
- package/dist/types/core/QualityMonitor.d.ts +2 -2
- package/dist/types/core/SeekingUtils.d.ts +2 -2
- package/dist/types/core/StreamStateClient.d.ts +2 -2
- package/dist/types/core/TelemetryReporter.d.ts +1 -1
- package/dist/types/core/TimerManager.d.ts +1 -1
- package/dist/types/core/detector.d.ts +1 -1
- package/dist/types/core/index.d.ts +44 -44
- package/dist/types/core/scorer.d.ts +1 -1
- package/dist/types/core/selector.d.ts +2 -2
- package/dist/types/index.d.ts +35 -34
- package/dist/types/players/DashJsPlayer.d.ts +3 -3
- package/dist/types/players/HlsJsPlayer.d.ts +3 -3
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +1 -1
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +1 -1
- package/dist/types/players/MewsWsPlayer/index.d.ts +2 -2
- package/dist/types/players/MewsWsPlayer/types.d.ts +15 -15
- package/dist/types/players/MistPlayer.d.ts +2 -2
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +3 -3
- package/dist/types/players/NativePlayer.d.ts +3 -3
- package/dist/types/players/VideoJsPlayer.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +2 -2
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +2 -2
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/index.d.ts +9 -9
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/types.d.ts +49 -49
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +31 -31
- package/dist/types/players/index.d.ts +5 -8
- package/dist/types/types.d.ts +15 -15
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +2 -2
- package/dist/types/vanilla/index.d.ts +4 -4
- package/dist/workers/decoder.worker.js +129 -122
- package/dist/workers/decoder.worker.js.map +1 -1
- package/package.json +31 -15
- package/src/core/ErrorClassifier.ts +499 -0
- package/src/core/PlayerController.ts +17 -2
- package/src/core/PlayerInterface.ts +109 -0
- package/src/core/PlayerManager.ts +290 -46
- package/src/core/PlayerRegistry.ts +221 -87
- package/src/core/TelemetryReporter.ts +4 -1
- package/src/index.ts +13 -4
- package/src/players/WebCodecsPlayer/index.ts +2 -2
- package/src/players/index.ts +5 -16
- package/src/styles/player.css +4 -1
- package/src/vanilla/FrameWorksPlayer.ts +2 -5
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import { getBrowserInfo, getBrowserCompatibility } from './detector.js';
|
|
2
|
+
import { ErrorCode } from './PlayerInterface.js';
|
|
3
|
+
import { ErrorClassifier } from './ErrorClassifier.js';
|
|
4
|
+
import { isProtocolBlacklisted, scorePlayer } from './scorer.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PlayerManager
|
|
8
|
+
*
|
|
9
|
+
* Central orchestrator for player selection and lifecycle management.
|
|
10
|
+
* Single source of truth for all scoring logic.
|
|
11
|
+
*
|
|
12
|
+
* Architecture:
|
|
13
|
+
* - `getAllCombinations()` is THE single function that computes player+source scores
|
|
14
|
+
* - Results are cached by content (source types + track codecs), not object identity
|
|
15
|
+
* - Events fire only when selection actually changes (no render spam)
|
|
16
|
+
* - `selectBestPlayer()` returns cached winner without recomputation
|
|
17
|
+
*/
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// PlayerManager Class
|
|
20
|
+
// ============================================================================
|
|
21
|
+
class PlayerManager {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.players = new Map();
|
|
24
|
+
this.currentPlayer = null;
|
|
25
|
+
this.listeners = new Map();
|
|
26
|
+
this.fallbackAttempts = 0;
|
|
27
|
+
// Caching: prevents recalculation on every render
|
|
28
|
+
this.cachedCombinations = null;
|
|
29
|
+
this.cachedSelection = null;
|
|
30
|
+
this.cacheKey = null;
|
|
31
|
+
this.lastLoggedWinner = null;
|
|
32
|
+
// Fallback state
|
|
33
|
+
this.lastContainer = null;
|
|
34
|
+
this.lastStreamInfo = null;
|
|
35
|
+
this.lastPlayerOptions = {};
|
|
36
|
+
this.lastManagerOptions = {};
|
|
37
|
+
this.excludedCombos = new Set();
|
|
38
|
+
// Serializes lifecycle operations to prevent race conditions
|
|
39
|
+
this.opQueue = Promise.resolve();
|
|
40
|
+
this.options = {
|
|
41
|
+
debug: false,
|
|
42
|
+
autoFallback: true,
|
|
43
|
+
maxFallbackAttempts: 3,
|
|
44
|
+
...options,
|
|
45
|
+
};
|
|
46
|
+
this.errorClassifier = new ErrorClassifier({
|
|
47
|
+
alternativesCount: 0,
|
|
48
|
+
debug: this.options.debug,
|
|
49
|
+
});
|
|
50
|
+
// Forward error classifier events to manager events
|
|
51
|
+
this.errorClassifier.on("recoveryAttempted", (data) => this.emit("recoveryAttempted", data));
|
|
52
|
+
this.errorClassifier.on("protocolSwapped", (data) => this.emit("protocolSwapped", data));
|
|
53
|
+
this.errorClassifier.on("qualityChanged", (data) => this.emit("qualityChanged", data));
|
|
54
|
+
this.errorClassifier.on("playbackFailed", (data) => this.emit("playbackFailed", data));
|
|
55
|
+
}
|
|
56
|
+
// ==========================================================================
|
|
57
|
+
// Player Registration
|
|
58
|
+
// ==========================================================================
|
|
59
|
+
registerPlayer(player) {
|
|
60
|
+
this.players.set(player.capability.shortname, player);
|
|
61
|
+
this.invalidateCache();
|
|
62
|
+
this.log(`Registered player: ${player.capability.name}`);
|
|
63
|
+
}
|
|
64
|
+
unregisterPlayer(shortname) {
|
|
65
|
+
const player = this.players.get(shortname);
|
|
66
|
+
if (player) {
|
|
67
|
+
player.destroy();
|
|
68
|
+
this.players.delete(shortname);
|
|
69
|
+
this.invalidateCache();
|
|
70
|
+
this.log(`Unregistered player: ${shortname}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
getRegisteredPlayers() {
|
|
74
|
+
return Array.from(this.players.values());
|
|
75
|
+
}
|
|
76
|
+
// ==========================================================================
|
|
77
|
+
// Caching
|
|
78
|
+
// ==========================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Compute cache key based on CONTENT, not object identity.
|
|
81
|
+
* Prevents recalculation when streamInfo is a new object with same data.
|
|
82
|
+
*/
|
|
83
|
+
computeCacheKey(streamInfo, mode) {
|
|
84
|
+
return JSON.stringify({
|
|
85
|
+
sources: streamInfo.source.map((s) => `${s.type}:${s.url ?? ""}`).sort(),
|
|
86
|
+
tracks: streamInfo.meta?.tracks?.map((t) => t.codec).sort() ?? [],
|
|
87
|
+
mode,
|
|
88
|
+
forcePlayer: this.options.forcePlayer,
|
|
89
|
+
forceSource: this.options.forceSource,
|
|
90
|
+
forceType: this.options.forceType,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
getComboKey(playerShortname, source) {
|
|
94
|
+
return `${playerShortname}:${source.type}:${source.url ?? ""}`;
|
|
95
|
+
}
|
|
96
|
+
/** Invalidate cache (called when player registrations change) */
|
|
97
|
+
invalidateCache() {
|
|
98
|
+
this.cachedCombinations = null;
|
|
99
|
+
this.cachedSelection = null;
|
|
100
|
+
this.cacheKey = null;
|
|
101
|
+
}
|
|
102
|
+
/** Get cached selection without recomputing */
|
|
103
|
+
getCurrentSelection() {
|
|
104
|
+
return this.cachedSelection;
|
|
105
|
+
}
|
|
106
|
+
/** Get cached combinations without recomputing */
|
|
107
|
+
getCachedCombinations() {
|
|
108
|
+
return this.cachedCombinations;
|
|
109
|
+
}
|
|
110
|
+
// ==========================================================================
|
|
111
|
+
// Selection Logic (Single Source of Truth)
|
|
112
|
+
// ==========================================================================
|
|
113
|
+
/**
|
|
114
|
+
* THE single source of truth for player+source scoring.
|
|
115
|
+
* Returns ALL combinations (compatible and incompatible) with scores.
|
|
116
|
+
* Results are cached - won't recompute if source types/tracks haven't changed.
|
|
117
|
+
*/
|
|
118
|
+
getAllCombinations(streamInfo, playbackMode) {
|
|
119
|
+
// Determine effective playback mode
|
|
120
|
+
const explicitMode = playbackMode || this.options.playbackMode;
|
|
121
|
+
const effectiveMode = explicitMode && explicitMode !== "auto"
|
|
122
|
+
? explicitMode
|
|
123
|
+
: streamInfo.type === "vod"
|
|
124
|
+
? "vod"
|
|
125
|
+
: "auto";
|
|
126
|
+
// Check cache
|
|
127
|
+
const key = this.computeCacheKey(streamInfo, effectiveMode);
|
|
128
|
+
if (key === this.cacheKey && this.cachedCombinations) {
|
|
129
|
+
return this.cachedCombinations;
|
|
130
|
+
}
|
|
131
|
+
// Cache miss - compute all combinations
|
|
132
|
+
const combinations = this.computeAllCombinations(streamInfo, effectiveMode);
|
|
133
|
+
// Update cache
|
|
134
|
+
this.cachedCombinations = combinations;
|
|
135
|
+
this.cacheKey = key;
|
|
136
|
+
// Update selection and emit events if changed
|
|
137
|
+
const newSelection = this.pickBestFromCombinations(combinations);
|
|
138
|
+
const selectionChanged = this.hasSelectionChanged(newSelection);
|
|
139
|
+
if (selectionChanged) {
|
|
140
|
+
this.cachedSelection = newSelection;
|
|
141
|
+
// Log only on actual change
|
|
142
|
+
if (this.options.debug && newSelection) {
|
|
143
|
+
const winnerKey = `${newSelection.player}:${newSelection.source?.type}`;
|
|
144
|
+
if (winnerKey !== this.lastLoggedWinner) {
|
|
145
|
+
console.log(`[PlayerManager] Selection: ${newSelection.player} + ${newSelection.source?.type} (score: ${newSelection.score.toFixed(3)})`);
|
|
146
|
+
this.lastLoggedWinner = winnerKey;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.emit("selection-changed", newSelection);
|
|
150
|
+
}
|
|
151
|
+
this.emit("combinations-updated", combinations);
|
|
152
|
+
return combinations;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Select the best player for given stream info.
|
|
156
|
+
* Uses cached combinations - won't recompute if data hasn't changed.
|
|
157
|
+
*/
|
|
158
|
+
selectBestPlayer(streamInfo, options) {
|
|
159
|
+
// Merge options
|
|
160
|
+
const mergedOptions = { ...this.options, ...options };
|
|
161
|
+
// Special handling for Legacy player - bypass normal selection
|
|
162
|
+
if (mergedOptions.forcePlayer === "mist-legacy" || mergedOptions.forceType === "mist/legacy") {
|
|
163
|
+
const legacyPlayer = this.players.get("mist-legacy");
|
|
164
|
+
if (legacyPlayer && streamInfo.source.length > 0) {
|
|
165
|
+
const firstSource = streamInfo.source[0];
|
|
166
|
+
const legacySource = {
|
|
167
|
+
url: firstSource.url,
|
|
168
|
+
type: "mist/legacy",
|
|
169
|
+
streamName: firstSource.streamName,
|
|
170
|
+
mistPlayerUrl: firstSource.mistPlayerUrl,
|
|
171
|
+
};
|
|
172
|
+
const result = {
|
|
173
|
+
score: 0.1,
|
|
174
|
+
player: "mist-legacy",
|
|
175
|
+
source: legacySource,
|
|
176
|
+
source_index: 0,
|
|
177
|
+
};
|
|
178
|
+
this.emit("playerSelected", {
|
|
179
|
+
player: result.player,
|
|
180
|
+
source: result.source,
|
|
181
|
+
score: result.score,
|
|
182
|
+
});
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Get combinations (will use cache if available)
|
|
187
|
+
const combinations = this.getAllCombinations(streamInfo, mergedOptions.playbackMode);
|
|
188
|
+
// Apply force filters
|
|
189
|
+
let filtered = combinations.filter((c) => c.compatible);
|
|
190
|
+
if (mergedOptions.forcePlayer) {
|
|
191
|
+
filtered = filtered.filter((c) => c.player === mergedOptions.forcePlayer);
|
|
192
|
+
}
|
|
193
|
+
if (mergedOptions.forceType) {
|
|
194
|
+
filtered = filtered.filter((c) => c.sourceType === mergedOptions.forceType);
|
|
195
|
+
}
|
|
196
|
+
if (mergedOptions.forceSource !== undefined) {
|
|
197
|
+
filtered = filtered.filter((c) => c.sourceIndex === mergedOptions.forceSource);
|
|
198
|
+
}
|
|
199
|
+
if (filtered.length === 0) {
|
|
200
|
+
this.log("No suitable player found");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const best = filtered[0];
|
|
204
|
+
const result = {
|
|
205
|
+
score: best.score,
|
|
206
|
+
player: best.player,
|
|
207
|
+
source: best.source,
|
|
208
|
+
source_index: best.sourceIndex,
|
|
209
|
+
};
|
|
210
|
+
this.emit("playerSelected", {
|
|
211
|
+
player: result.player,
|
|
212
|
+
source: result.source,
|
|
213
|
+
score: result.score,
|
|
214
|
+
});
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Internal: compute all combinations (no caching)
|
|
219
|
+
*/
|
|
220
|
+
computeAllCombinations(streamInfo, effectiveMode) {
|
|
221
|
+
const combinations = [];
|
|
222
|
+
const players = Array.from(this.players.values());
|
|
223
|
+
const maxPriority = Math.max(...players.map((p) => p.capability.priority), 1);
|
|
224
|
+
// Filter blacklisted sources for scoring index calculation
|
|
225
|
+
const selectionSources = streamInfo.source.filter((s) => !isProtocolBlacklisted(s.type));
|
|
226
|
+
const selectionIndexBySource = new Map();
|
|
227
|
+
selectionSources.forEach((s, idx) => selectionIndexBySource.set(s, idx));
|
|
228
|
+
const totalSources = selectionSources.length;
|
|
229
|
+
const requiredTracks = [];
|
|
230
|
+
if (streamInfo.meta.tracks.some((t) => t.type === "video")) {
|
|
231
|
+
requiredTracks.push("video");
|
|
232
|
+
}
|
|
233
|
+
if (streamInfo.meta.tracks.some((t) => t.type === "audio")) {
|
|
234
|
+
requiredTracks.push("audio");
|
|
235
|
+
}
|
|
236
|
+
// Track seen player+sourceType pairs to avoid duplicates
|
|
237
|
+
const seenPairs = new Set();
|
|
238
|
+
for (const player of players) {
|
|
239
|
+
for (let sourceIndex = 0; sourceIndex < streamInfo.source.length; sourceIndex++) {
|
|
240
|
+
const source = streamInfo.source[sourceIndex];
|
|
241
|
+
const pairKey = this.getComboKey(player.capability.shortname, source);
|
|
242
|
+
// Skip duplicate player+sourceType combinations
|
|
243
|
+
if (seenPairs.has(pairKey))
|
|
244
|
+
continue;
|
|
245
|
+
seenPairs.add(pairKey);
|
|
246
|
+
// Blacklisted protocols: show as incompatible
|
|
247
|
+
const sourceListIndex = selectionIndexBySource.get(source);
|
|
248
|
+
if (sourceListIndex === undefined) {
|
|
249
|
+
combinations.push({
|
|
250
|
+
player: player.capability.shortname,
|
|
251
|
+
playerName: player.capability.name,
|
|
252
|
+
source,
|
|
253
|
+
sourceIndex,
|
|
254
|
+
sourceType: source.type,
|
|
255
|
+
score: 0,
|
|
256
|
+
compatible: false,
|
|
257
|
+
incompatibleReason: `Protocol "${source.type}" is blacklisted`,
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// Check MIME support
|
|
262
|
+
const mimeSupported = player.isMimeSupported(source.type);
|
|
263
|
+
if (!mimeSupported) {
|
|
264
|
+
combinations.push({
|
|
265
|
+
player: player.capability.shortname,
|
|
266
|
+
playerName: player.capability.name,
|
|
267
|
+
source,
|
|
268
|
+
sourceIndex,
|
|
269
|
+
sourceType: source.type,
|
|
270
|
+
score: 0,
|
|
271
|
+
compatible: false,
|
|
272
|
+
incompatibleReason: `MIME type "${source.type}" not supported`,
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
// Check browser/codec compatibility
|
|
277
|
+
const tracktypes = player.isBrowserSupported(source.type, source, streamInfo);
|
|
278
|
+
if (!tracktypes) {
|
|
279
|
+
// Codec incompatible - still score for UI display
|
|
280
|
+
const priorityScore = 1 - player.capability.priority / Math.max(maxPriority, 1);
|
|
281
|
+
const sourceScore = 1 - sourceListIndex / Math.max(totalSources - 1, 1);
|
|
282
|
+
const playerScore = scorePlayer(["video", "audio"], player.capability.priority, sourceListIndex, {
|
|
283
|
+
maxPriority,
|
|
284
|
+
totalSources,
|
|
285
|
+
playerShortname: player.capability.shortname,
|
|
286
|
+
mimeType: source.type,
|
|
287
|
+
playbackMode: effectiveMode,
|
|
288
|
+
});
|
|
289
|
+
combinations.push({
|
|
290
|
+
player: player.capability.shortname,
|
|
291
|
+
playerName: player.capability.name,
|
|
292
|
+
source,
|
|
293
|
+
sourceIndex,
|
|
294
|
+
sourceType: source.type,
|
|
295
|
+
score: playerScore.total,
|
|
296
|
+
compatible: false,
|
|
297
|
+
codecIncompatible: true,
|
|
298
|
+
incompatibleReason: "Codec not supported by browser",
|
|
299
|
+
scoreBreakdown: {
|
|
300
|
+
trackScore: 0,
|
|
301
|
+
trackTypes: [],
|
|
302
|
+
priorityScore,
|
|
303
|
+
sourceScore,
|
|
304
|
+
weights: { tracks: 0.5, priority: 0.1, source: 0.05 },
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (Array.isArray(tracktypes) && requiredTracks.length > 0) {
|
|
310
|
+
const missing = requiredTracks.filter((t) => !tracktypes.includes(t));
|
|
311
|
+
if (missing.length > 0) {
|
|
312
|
+
const priorityScore = 1 - player.capability.priority / Math.max(maxPriority, 1);
|
|
313
|
+
const sourceScore = 1 - sourceListIndex / Math.max(totalSources - 1, 1);
|
|
314
|
+
const playerScore = scorePlayer(tracktypes, player.capability.priority, sourceListIndex, {
|
|
315
|
+
maxPriority,
|
|
316
|
+
totalSources,
|
|
317
|
+
playerShortname: player.capability.shortname,
|
|
318
|
+
mimeType: source.type,
|
|
319
|
+
playbackMode: effectiveMode,
|
|
320
|
+
});
|
|
321
|
+
combinations.push({
|
|
322
|
+
player: player.capability.shortname,
|
|
323
|
+
playerName: player.capability.name,
|
|
324
|
+
source,
|
|
325
|
+
sourceIndex,
|
|
326
|
+
sourceType: source.type,
|
|
327
|
+
score: playerScore.total,
|
|
328
|
+
compatible: false,
|
|
329
|
+
incompatibleReason: `Missing required tracks: ${missing.join(", ")}`,
|
|
330
|
+
scoreBreakdown: {
|
|
331
|
+
trackScore: 0,
|
|
332
|
+
trackTypes: tracktypes,
|
|
333
|
+
priorityScore,
|
|
334
|
+
sourceScore,
|
|
335
|
+
weights: { tracks: 0.5, priority: 0.1, source: 0.05 },
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Compatible - calculate full score
|
|
342
|
+
const trackScore = Array.isArray(tracktypes)
|
|
343
|
+
? tracktypes.reduce((sum, t) => sum + ({ video: 2.0, audio: 1.0, subtitle: 0.5 }[t] || 0), 0)
|
|
344
|
+
: 1.9;
|
|
345
|
+
const priorityScore = 1 - player.capability.priority / Math.max(maxPriority, 1);
|
|
346
|
+
const sourceScore = 1 - sourceListIndex / Math.max(totalSources - 1, 1);
|
|
347
|
+
const playerScore = scorePlayer(tracktypes, player.capability.priority, sourceListIndex, {
|
|
348
|
+
maxPriority,
|
|
349
|
+
totalSources,
|
|
350
|
+
playerShortname: player.capability.shortname,
|
|
351
|
+
mimeType: source.type,
|
|
352
|
+
playbackMode: effectiveMode,
|
|
353
|
+
});
|
|
354
|
+
combinations.push({
|
|
355
|
+
player: player.capability.shortname,
|
|
356
|
+
playerName: player.capability.name,
|
|
357
|
+
source,
|
|
358
|
+
sourceIndex,
|
|
359
|
+
sourceType: source.type,
|
|
360
|
+
score: playerScore.total,
|
|
361
|
+
compatible: true,
|
|
362
|
+
scoreBreakdown: {
|
|
363
|
+
trackScore,
|
|
364
|
+
trackTypes: Array.isArray(tracktypes) ? tracktypes : ["video", "audio"],
|
|
365
|
+
priorityScore,
|
|
366
|
+
sourceScore,
|
|
367
|
+
reliabilityScore: playerScore.breakdown?.reliabilityScore ?? 0,
|
|
368
|
+
modeBonus: playerScore.breakdown?.modeBonus ?? 0,
|
|
369
|
+
routingBonus: playerScore.breakdown?.routingBonus ?? 0,
|
|
370
|
+
weights: {
|
|
371
|
+
tracks: 0.5,
|
|
372
|
+
priority: 0.1,
|
|
373
|
+
source: 0.05,
|
|
374
|
+
reliability: 0.1,
|
|
375
|
+
mode: 0.12,
|
|
376
|
+
routing: 0.08,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Add Legacy player option
|
|
383
|
+
const legacyPlayer = this.players.get("mist-legacy");
|
|
384
|
+
if (legacyPlayer && streamInfo.source.length > 0) {
|
|
385
|
+
const firstSource = streamInfo.source[0];
|
|
386
|
+
const legacySource = {
|
|
387
|
+
url: firstSource.url,
|
|
388
|
+
type: "mist/legacy",
|
|
389
|
+
streamName: firstSource.streamName,
|
|
390
|
+
mistPlayerUrl: firstSource.mistPlayerUrl,
|
|
391
|
+
};
|
|
392
|
+
combinations.push({
|
|
393
|
+
player: legacyPlayer.capability.shortname,
|
|
394
|
+
playerName: legacyPlayer.capability.name,
|
|
395
|
+
source: legacySource,
|
|
396
|
+
sourceIndex: 0,
|
|
397
|
+
sourceType: "mist/legacy",
|
|
398
|
+
score: 0.1,
|
|
399
|
+
compatible: true,
|
|
400
|
+
scoreBreakdown: {
|
|
401
|
+
trackScore: 2.0,
|
|
402
|
+
trackTypes: ["video", "audio"],
|
|
403
|
+
priorityScore: 0,
|
|
404
|
+
sourceScore: 0,
|
|
405
|
+
weights: { tracks: 0.5, priority: 0.1, source: 0.05 },
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
// Sort: compatible first by score descending, then incompatible alphabetically
|
|
410
|
+
return combinations.sort((a, b) => {
|
|
411
|
+
if (a.compatible !== b.compatible)
|
|
412
|
+
return a.compatible ? -1 : 1;
|
|
413
|
+
if (a.compatible)
|
|
414
|
+
return b.score - a.score;
|
|
415
|
+
return a.playerName.localeCompare(b.playerName);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
diagnoseNoPlayersAvailable(streamInfo, combinations) {
|
|
419
|
+
const allSources = streamInfo.source ?? [];
|
|
420
|
+
const blacklistedSources = allSources.filter((source) => isProtocolBlacklisted(source.type));
|
|
421
|
+
const blacklistedProtocols = Array.from(new Set(blacklistedSources.map((source) => source.type)));
|
|
422
|
+
if (allSources.length > 0 && blacklistedSources.length === allSources.length) {
|
|
423
|
+
return {
|
|
424
|
+
code: ErrorCode.ALL_PROTOCOLS_BLACKLISTED,
|
|
425
|
+
message: `All ${allSources.length} protocols are blacklisted`,
|
|
426
|
+
details: {
|
|
427
|
+
blacklistedProtocols,
|
|
428
|
+
incompatibilityReasons: [
|
|
429
|
+
`All source protocols are blacklisted: ${blacklistedProtocols.join(", ")}`,
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const incompatibilityReasons = Array.from(new Set(combinations
|
|
435
|
+
.filter((combo) => !combo.compatible && combo.incompatibleReason)
|
|
436
|
+
.map((combo) => combo.incompatibleReason)));
|
|
437
|
+
if (allSources.length === 0) {
|
|
438
|
+
return {
|
|
439
|
+
code: ErrorCode.ALL_PROTOCOLS_EXHAUSTED,
|
|
440
|
+
message: "No playback sources provided",
|
|
441
|
+
details: {
|
|
442
|
+
incompatibilityReasons,
|
|
443
|
+
blacklistedProtocols,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (incompatibilityReasons.length === 1) {
|
|
448
|
+
return {
|
|
449
|
+
code: ErrorCode.ALL_PROTOCOLS_EXHAUSTED,
|
|
450
|
+
message: incompatibilityReasons[0],
|
|
451
|
+
details: {
|
|
452
|
+
incompatibilityReasons,
|
|
453
|
+
blacklistedProtocols,
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
code: ErrorCode.ALL_PROTOCOLS_EXHAUSTED,
|
|
459
|
+
message: "No compatible player/protocol combinations",
|
|
460
|
+
details: {
|
|
461
|
+
incompatibilityReasons: incompatibilityReasons.slice(0, 5),
|
|
462
|
+
blacklistedProtocols,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Pick best compatible combination
|
|
468
|
+
*/
|
|
469
|
+
pickBestFromCombinations(combinations) {
|
|
470
|
+
const compatible = combinations.filter((c) => c.compatible);
|
|
471
|
+
if (compatible.length === 0)
|
|
472
|
+
return null;
|
|
473
|
+
const best = compatible[0];
|
|
474
|
+
return {
|
|
475
|
+
score: best.score,
|
|
476
|
+
player: best.player,
|
|
477
|
+
source: best.source,
|
|
478
|
+
source_index: best.sourceIndex,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Check if selection changed
|
|
483
|
+
*/
|
|
484
|
+
hasSelectionChanged(newSelection) {
|
|
485
|
+
if (!this.cachedSelection && !newSelection)
|
|
486
|
+
return false;
|
|
487
|
+
if (!this.cachedSelection || !newSelection)
|
|
488
|
+
return true;
|
|
489
|
+
return (this.cachedSelection.player !== newSelection.player ||
|
|
490
|
+
this.cachedSelection.source?.type !== newSelection.source?.type ||
|
|
491
|
+
(this.cachedSelection.source?.url ?? "") !== (newSelection.source?.url ?? ""));
|
|
492
|
+
}
|
|
493
|
+
// ==========================================================================
|
|
494
|
+
// Player Initialization
|
|
495
|
+
// ==========================================================================
|
|
496
|
+
enqueueOp(op) {
|
|
497
|
+
const run = this.opQueue.then(op, op);
|
|
498
|
+
this.opQueue = run.then(() => undefined, () => undefined);
|
|
499
|
+
return run;
|
|
500
|
+
}
|
|
501
|
+
async initializePlayer(container, streamInfo, playerOptions = {}, managerOptions) {
|
|
502
|
+
this.log("initializePlayer() called");
|
|
503
|
+
return this.enqueueOp(async () => {
|
|
504
|
+
this.log("Inside enqueueOp - starting");
|
|
505
|
+
this.fallbackAttempts = 0;
|
|
506
|
+
this.excludedCombos.clear();
|
|
507
|
+
this.errorClassifier.reset();
|
|
508
|
+
// Save for fallback (strip force settings - they're one-shot, not for fallback)
|
|
509
|
+
this.lastContainer = container;
|
|
510
|
+
this.lastStreamInfo = streamInfo;
|
|
511
|
+
this.lastPlayerOptions = playerOptions;
|
|
512
|
+
// Keep playback mode (persistent preference) but clear force settings
|
|
513
|
+
this.lastManagerOptions = {
|
|
514
|
+
playbackMode: managerOptions?.playbackMode,
|
|
515
|
+
debug: managerOptions?.debug,
|
|
516
|
+
autoFallback: managerOptions?.autoFallback,
|
|
517
|
+
maxFallbackAttempts: managerOptions?.maxFallbackAttempts,
|
|
518
|
+
// forcePlayer, forceType, forceSource are intentionally NOT saved
|
|
519
|
+
// They are one-shot selections that shouldn't persist through fallback
|
|
520
|
+
};
|
|
521
|
+
return this.tryInitializePlayer(container, streamInfo, playerOptions, managerOptions);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async tryInitializePlayer(container, streamInfo, playerOptions, managerOptions, excludeCombos = new Set()) {
|
|
525
|
+
this.log("tryInitializePlayer() starting");
|
|
526
|
+
// Clean up previous player
|
|
527
|
+
if (this.currentPlayer) {
|
|
528
|
+
this.log("Cleaning up previous player...");
|
|
529
|
+
await Promise.resolve(this.currentPlayer.destroy());
|
|
530
|
+
this.currentPlayer = null;
|
|
531
|
+
}
|
|
532
|
+
container.innerHTML = "";
|
|
533
|
+
// Update classifier with current alternatives count
|
|
534
|
+
const allCombinations = this.getAllCombinations(streamInfo, managerOptions?.playbackMode);
|
|
535
|
+
const compatibleCombos = allCombinations.filter((c) => c.compatible && !excludeCombos.has(this.getComboKey(c.player, c.source)));
|
|
536
|
+
this.errorClassifier.setAlternativesRemaining(Math.max(0, compatibleCombos.length - 1));
|
|
537
|
+
// Filter excluded combinations
|
|
538
|
+
const availableSources = streamInfo.source.filter((_, index) => {
|
|
539
|
+
if (excludeCombos.size === 0)
|
|
540
|
+
return true;
|
|
541
|
+
const selection = this.selectBestPlayer({ ...streamInfo, source: [streamInfo.source[index]] }, managerOptions);
|
|
542
|
+
return selection && !excludeCombos.has(this.getComboKey(selection.player, selection.source));
|
|
543
|
+
});
|
|
544
|
+
if (availableSources.length === 0) {
|
|
545
|
+
this.log("No available sources after filtering");
|
|
546
|
+
const diagnostic = this.diagnoseNoPlayersAvailable(streamInfo, allCombinations);
|
|
547
|
+
const action = this.errorClassifier.classifyWithDetails(diagnostic.code, diagnostic.message, diagnostic.details);
|
|
548
|
+
if (action.type === "fatal") {
|
|
549
|
+
throw new Error(diagnostic.message);
|
|
550
|
+
}
|
|
551
|
+
throw new Error(diagnostic.message);
|
|
552
|
+
}
|
|
553
|
+
this.log(`Available sources: ${availableSources.length}`);
|
|
554
|
+
const modifiedStreamInfo = { ...streamInfo, source: availableSources };
|
|
555
|
+
const selection = this.selectBestPlayer(modifiedStreamInfo, managerOptions);
|
|
556
|
+
if (!selection) {
|
|
557
|
+
this.log("No suitable player selected");
|
|
558
|
+
const selectionCombinations = this.getAllCombinations(modifiedStreamInfo, managerOptions?.playbackMode);
|
|
559
|
+
const diagnostic = this.diagnoseNoPlayersAvailable(modifiedStreamInfo, selectionCombinations);
|
|
560
|
+
this.errorClassifier.classifyWithDetails(diagnostic.code, diagnostic.message, diagnostic.details);
|
|
561
|
+
throw new Error(diagnostic.message);
|
|
562
|
+
}
|
|
563
|
+
this.log(`Selected: ${selection.player} for ${selection.source.type}`);
|
|
564
|
+
const player = this.players.get(selection.player);
|
|
565
|
+
if (!player) {
|
|
566
|
+
this.log(`Player ${selection.player} not registered`);
|
|
567
|
+
throw new Error(`Player ${selection.player} not found`);
|
|
568
|
+
}
|
|
569
|
+
this.log(`Calling ${selection.player}.initialize()...`);
|
|
570
|
+
try {
|
|
571
|
+
const videoElement = await player.initialize(container, selection.source, playerOptions, streamInfo);
|
|
572
|
+
this.log(`${selection.player}.initialize() completed successfully`);
|
|
573
|
+
this.currentPlayer = player;
|
|
574
|
+
this.errorClassifier.reset();
|
|
575
|
+
this.emit("playerInitialized", { player, videoElement });
|
|
576
|
+
return videoElement;
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
return this.handleInitError(error, selection, container, streamInfo, playerOptions, managerOptions, excludeCombos);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Handle initialization error using ErrorClassifier to determine recovery action.
|
|
584
|
+
*/
|
|
585
|
+
async handleInitError(error, selection, container, streamInfo, playerOptions, managerOptions, excludeCombos) {
|
|
586
|
+
const errorCode = ErrorClassifier.mapErrorToCode(error instanceof Error ? error : new Error(String(error)));
|
|
587
|
+
const action = this.errorClassifier.classify(errorCode, error instanceof Error ? error : String(error));
|
|
588
|
+
this.log(`Error classified: ${errorCode}, action: ${action.type}`);
|
|
589
|
+
switch (action.type) {
|
|
590
|
+
case "retry": {
|
|
591
|
+
this.log(`Retrying in ${action.delayMs}ms...`);
|
|
592
|
+
await this.delay(action.delayMs);
|
|
593
|
+
return this.tryInitializePlayer(container, streamInfo, playerOptions, managerOptions, excludeCombos);
|
|
594
|
+
}
|
|
595
|
+
case "swap": {
|
|
596
|
+
const maxAttempts = this.options.maxFallbackAttempts || 3;
|
|
597
|
+
if (!this.options.autoFallback || this.fallbackAttempts >= maxAttempts) {
|
|
598
|
+
this.errorClassifier.classify(ErrorCode.ALL_PROTOCOLS_EXHAUSTED);
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
this.fallbackAttempts++;
|
|
602
|
+
const previousPlayer = selection.player;
|
|
603
|
+
const previousProtocol = selection.source.type;
|
|
604
|
+
excludeCombos.add(this.getComboKey(selection.player, selection.source));
|
|
605
|
+
this.log(`Swapping from ${previousPlayer} (attempt ${this.fallbackAttempts}/${maxAttempts})`);
|
|
606
|
+
try {
|
|
607
|
+
const result = await this.tryInitializePlayer(container, streamInfo, playerOptions, managerOptions, excludeCombos);
|
|
608
|
+
// Notify classifier and emit toast event for successful swap
|
|
609
|
+
const newPlayer = this.currentPlayer?.capability.shortname || "unknown";
|
|
610
|
+
const newProtocol = this.cachedSelection?.source.type || "unknown";
|
|
611
|
+
this.errorClassifier.notifyProtocolSwap(previousPlayer, newPlayer, previousProtocol, newProtocol, action.reason);
|
|
612
|
+
this.emit("fallbackAttempted", {
|
|
613
|
+
fromPlayer: previousPlayer,
|
|
614
|
+
toPlayer: newPlayer,
|
|
615
|
+
});
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
catch (swapError) {
|
|
619
|
+
throw swapError;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
case "fatal":
|
|
623
|
+
default:
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
delay(ms) {
|
|
628
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
629
|
+
}
|
|
630
|
+
// ==========================================================================
|
|
631
|
+
// Fallback Management
|
|
632
|
+
// ==========================================================================
|
|
633
|
+
async tryPlaybackFallback() {
|
|
634
|
+
return this.enqueueOp(async () => {
|
|
635
|
+
if (!this.lastContainer || !this.lastStreamInfo) {
|
|
636
|
+
this.log("Cannot attempt fallback: no previous init params");
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
const maxAttempts = this.options.maxFallbackAttempts || 3;
|
|
640
|
+
if (this.fallbackAttempts >= maxAttempts) {
|
|
641
|
+
this.log(`Fallback exhausted (${this.fallbackAttempts}/${maxAttempts})`);
|
|
642
|
+
this.errorClassifier.classify(ErrorCode.ALL_PROTOCOLS_EXHAUSTED);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
const previousPlayer = this.currentPlayer?.capability.shortname || "unknown";
|
|
646
|
+
const previousProtocol = this.cachedSelection?.source.type || "unknown";
|
|
647
|
+
if (this.currentPlayer) {
|
|
648
|
+
if (this.cachedSelection) {
|
|
649
|
+
this.excludedCombos.add(this.getComboKey(this.cachedSelection.player, this.cachedSelection.source));
|
|
650
|
+
}
|
|
651
|
+
await Promise.resolve(this.currentPlayer.destroy());
|
|
652
|
+
this.currentPlayer = null;
|
|
653
|
+
}
|
|
654
|
+
this.fallbackAttempts++;
|
|
655
|
+
this.lastContainer.innerHTML = "";
|
|
656
|
+
this.errorClassifier.reset();
|
|
657
|
+
try {
|
|
658
|
+
await this.tryInitializePlayer(this.lastContainer, this.lastStreamInfo, this.lastPlayerOptions, this.lastManagerOptions, this.excludedCombos);
|
|
659
|
+
const current = this.getCurrentPlayer();
|
|
660
|
+
const newPlayer = current?.capability.shortname || "unknown";
|
|
661
|
+
const newProtocol = this.cachedSelection?.source.type || "unknown";
|
|
662
|
+
this.errorClassifier.notifyProtocolSwap(previousPlayer, newPlayer, previousProtocol, newProtocol, "Playback fallback");
|
|
663
|
+
this.emit("fallbackAttempted", {
|
|
664
|
+
fromPlayer: previousPlayer,
|
|
665
|
+
toPlayer: newPlayer,
|
|
666
|
+
});
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
this.log("Playback fallback failed");
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Report an error from a player for classification and potential recovery.
|
|
677
|
+
* Players should call this instead of emitting errors directly.
|
|
678
|
+
*/
|
|
679
|
+
reportError(error) {
|
|
680
|
+
const errorCode = ErrorClassifier.mapErrorToCode(error);
|
|
681
|
+
return this.errorClassifier.classify(errorCode, error);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Report a quality change (for ABR quality drops).
|
|
685
|
+
* UI layer can call this to trigger toast notification.
|
|
686
|
+
*/
|
|
687
|
+
reportQualityChange(direction, reason) {
|
|
688
|
+
this.emit("qualityChanged", { direction, reason });
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Get the error classifier for direct access (advanced use).
|
|
692
|
+
*/
|
|
693
|
+
getErrorClassifier() {
|
|
694
|
+
return this.errorClassifier;
|
|
695
|
+
}
|
|
696
|
+
getRemainingFallbackAttempts() {
|
|
697
|
+
return Math.max(0, (this.options.maxFallbackAttempts || 3) - this.fallbackAttempts);
|
|
698
|
+
}
|
|
699
|
+
canAttemptFallback() {
|
|
700
|
+
return this.getRemainingFallbackAttempts() > 0 && this.lastStreamInfo !== null;
|
|
701
|
+
}
|
|
702
|
+
getCurrentPlayer() {
|
|
703
|
+
return this.currentPlayer;
|
|
704
|
+
}
|
|
705
|
+
// ==========================================================================
|
|
706
|
+
// Browser Capabilities
|
|
707
|
+
// ==========================================================================
|
|
708
|
+
getBrowserCapabilities() {
|
|
709
|
+
const browser = getBrowserInfo();
|
|
710
|
+
const compatibility = getBrowserCompatibility();
|
|
711
|
+
return {
|
|
712
|
+
browser,
|
|
713
|
+
compatibility,
|
|
714
|
+
supportedMimeTypes: this.getSupportedMimeTypes(),
|
|
715
|
+
availablePlayers: this.getAvailablePlayerInfo(),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
getSupportedMimeTypes() {
|
|
719
|
+
const mimes = new Set();
|
|
720
|
+
for (const player of this.players.values()) {
|
|
721
|
+
player.capability.mimes.forEach((mime) => mimes.add(mime));
|
|
722
|
+
}
|
|
723
|
+
return Array.from(mimes).sort();
|
|
724
|
+
}
|
|
725
|
+
getAvailablePlayerInfo() {
|
|
726
|
+
return Array.from(this.players.values())
|
|
727
|
+
.map((player) => ({
|
|
728
|
+
name: player.capability.name,
|
|
729
|
+
shortname: player.capability.shortname,
|
|
730
|
+
priority: player.capability.priority,
|
|
731
|
+
mimes: player.capability.mimes,
|
|
732
|
+
}))
|
|
733
|
+
.sort((a, b) => a.priority - b.priority);
|
|
734
|
+
}
|
|
735
|
+
// ==========================================================================
|
|
736
|
+
// Lifecycle
|
|
737
|
+
// ==========================================================================
|
|
738
|
+
async destroy() {
|
|
739
|
+
await this.enqueueOp(async () => {
|
|
740
|
+
if (this.currentPlayer) {
|
|
741
|
+
await Promise.resolve(this.currentPlayer.destroy());
|
|
742
|
+
this.currentPlayer = null;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
removeAllListeners() {
|
|
747
|
+
this.listeners.clear();
|
|
748
|
+
}
|
|
749
|
+
// ==========================================================================
|
|
750
|
+
// Event System
|
|
751
|
+
// ==========================================================================
|
|
752
|
+
on(event, listener) {
|
|
753
|
+
if (!this.listeners.has(event)) {
|
|
754
|
+
this.listeners.set(event, new Set());
|
|
755
|
+
}
|
|
756
|
+
this.listeners.get(event).add(listener);
|
|
757
|
+
// Return unsubscribe function
|
|
758
|
+
return () => this.off(event, listener);
|
|
759
|
+
}
|
|
760
|
+
off(event, listener) {
|
|
761
|
+
this.listeners.get(event)?.delete(listener);
|
|
762
|
+
}
|
|
763
|
+
emit(event, data) {
|
|
764
|
+
this.listeners.get(event)?.forEach((listener) => {
|
|
765
|
+
try {
|
|
766
|
+
listener(data);
|
|
767
|
+
}
|
|
768
|
+
catch (e) {
|
|
769
|
+
console.error(`Error in PlayerManager ${event} listener:`, e);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// ==========================================================================
|
|
774
|
+
// Logging
|
|
775
|
+
// ==========================================================================
|
|
776
|
+
log(message) {
|
|
777
|
+
if (this.options.debug) {
|
|
778
|
+
console.log(`[PlayerManager] ${message}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// ==========================================================================
|
|
782
|
+
// Testing
|
|
783
|
+
// ==========================================================================
|
|
784
|
+
async testSource(source, streamInfo) {
|
|
785
|
+
const testStreamInfo = { ...streamInfo, source: [source] };
|
|
786
|
+
const selection = this.selectBestPlayer(testStreamInfo);
|
|
787
|
+
if (!selection) {
|
|
788
|
+
return { canPlay: false, players: [] };
|
|
789
|
+
}
|
|
790
|
+
const capablePlayers = [];
|
|
791
|
+
for (const player of this.players.values()) {
|
|
792
|
+
if (player.isMimeSupported(source.type)) {
|
|
793
|
+
const browserSupport = player.isBrowserSupported(source.type, source, streamInfo);
|
|
794
|
+
if (browserSupport) {
|
|
795
|
+
capablePlayers.push(player.capability.shortname);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return { canPlay: true, players: capablePlayers };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export { PlayerManager };
|
|
804
|
+
//# sourceMappingURL=PlayerManager.js.map
|