@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.
Files changed (247) hide show
  1. package/dist/cjs/core/ABRController.js +456 -0
  2. package/dist/cjs/core/ABRController.js.map +1 -0
  3. package/dist/cjs/core/CodecUtils.js +195 -0
  4. package/dist/cjs/core/CodecUtils.js.map +1 -0
  5. package/dist/cjs/core/ErrorClassifier.js +410 -0
  6. package/dist/cjs/core/ErrorClassifier.js.map +1 -0
  7. package/dist/cjs/core/EventEmitter.js +108 -0
  8. package/dist/cjs/core/EventEmitter.js.map +1 -0
  9. package/dist/cjs/core/GatewayClient.js +342 -0
  10. package/dist/cjs/core/GatewayClient.js.map +1 -0
  11. package/dist/cjs/core/InteractionController.js +606 -0
  12. package/dist/cjs/core/InteractionController.js.map +1 -0
  13. package/dist/cjs/core/LiveDurationProxy.js +186 -0
  14. package/dist/cjs/core/LiveDurationProxy.js.map +1 -0
  15. package/dist/cjs/core/MetaTrackManager.js +624 -0
  16. package/dist/cjs/core/MetaTrackManager.js.map +1 -0
  17. package/dist/cjs/core/MistReporter.js +449 -0
  18. package/dist/cjs/core/MistReporter.js.map +1 -0
  19. package/dist/cjs/core/MistSignaling.js +264 -0
  20. package/dist/cjs/core/MistSignaling.js.map +1 -0
  21. package/dist/cjs/core/PlayerController.js +2658 -0
  22. package/dist/cjs/core/PlayerController.js.map +1 -0
  23. package/dist/cjs/core/PlayerInterface.js +269 -0
  24. package/dist/cjs/core/PlayerInterface.js.map +1 -0
  25. package/dist/cjs/core/PlayerManager.js +806 -0
  26. package/dist/cjs/core/PlayerManager.js.map +1 -0
  27. package/dist/cjs/core/PlayerRegistry.js +270 -0
  28. package/dist/cjs/core/PlayerRegistry.js.map +1 -0
  29. package/dist/cjs/core/QualityMonitor.js +474 -0
  30. package/dist/cjs/core/QualityMonitor.js.map +1 -0
  31. package/dist/cjs/core/SeekingUtils.js +292 -0
  32. package/dist/cjs/core/SeekingUtils.js.map +1 -0
  33. package/dist/cjs/core/StreamStateClient.js +381 -0
  34. package/dist/cjs/core/StreamStateClient.js.map +1 -0
  35. package/dist/cjs/core/SubtitleManager.js +227 -0
  36. package/dist/cjs/core/SubtitleManager.js.map +1 -0
  37. package/dist/cjs/core/TelemetryReporter.js +258 -0
  38. package/dist/cjs/core/TelemetryReporter.js.map +1 -0
  39. package/dist/cjs/core/TimeFormat.js +176 -0
  40. package/dist/cjs/core/TimeFormat.js.map +1 -0
  41. package/dist/cjs/core/TimerManager.js +176 -0
  42. package/dist/cjs/core/TimerManager.js.map +1 -0
  43. package/dist/cjs/core/UrlUtils.js +160 -0
  44. package/dist/cjs/core/UrlUtils.js.map +1 -0
  45. package/dist/cjs/core/detector.js +293 -0
  46. package/dist/cjs/core/detector.js.map +1 -0
  47. package/dist/cjs/core/scorer.js +443 -0
  48. package/dist/cjs/core/scorer.js.map +1 -0
  49. package/dist/cjs/index.js +121 -20134
  50. package/dist/cjs/index.js.map +1 -1
  51. package/dist/cjs/lib/utils.js +11 -0
  52. package/dist/cjs/lib/utils.js.map +1 -0
  53. package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +6 -0
  54. package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
  55. package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3042 -0
  56. package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
  57. package/dist/cjs/players/DashJsPlayer.js +638 -0
  58. package/dist/cjs/players/DashJsPlayer.js.map +1 -0
  59. package/dist/cjs/players/HlsJsPlayer.js +482 -0
  60. package/dist/cjs/players/HlsJsPlayer.js.map +1 -0
  61. package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js +522 -0
  62. package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
  63. package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js +215 -0
  64. package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
  65. package/dist/cjs/players/MewsWsPlayer/index.js +987 -0
  66. package/dist/cjs/players/MewsWsPlayer/index.js.map +1 -0
  67. package/dist/cjs/players/MistPlayer.js +185 -0
  68. package/dist/cjs/players/MistPlayer.js.map +1 -0
  69. package/dist/cjs/players/MistWebRTCPlayer/index.js +635 -0
  70. package/dist/cjs/players/MistWebRTCPlayer/index.js.map +1 -0
  71. package/dist/cjs/players/NativePlayer.js +762 -0
  72. package/dist/cjs/players/NativePlayer.js.map +1 -0
  73. package/dist/cjs/players/VideoJsPlayer.js +585 -0
  74. package/dist/cjs/players/VideoJsPlayer.js.map +1 -0
  75. package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js +236 -0
  76. package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
  77. package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js +143 -0
  78. package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
  79. package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js +96 -0
  80. package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
  81. package/dist/cjs/players/WebCodecsPlayer/SyncController.js +359 -0
  82. package/dist/cjs/players/WebCodecsPlayer/SyncController.js.map +1 -0
  83. package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js +460 -0
  84. package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
  85. package/dist/cjs/players/WebCodecsPlayer/index.js +1467 -0
  86. package/dist/cjs/players/WebCodecsPlayer/index.js.map +1 -0
  87. package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +320 -0
  88. package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
  89. package/dist/cjs/styles/index.js +57 -0
  90. package/dist/cjs/styles/index.js.map +1 -0
  91. package/dist/cjs/vanilla/FrameWorksPlayer.js +269 -0
  92. package/dist/cjs/vanilla/FrameWorksPlayer.js.map +1 -0
  93. package/dist/cjs/vanilla.js +11 -0
  94. package/dist/cjs/vanilla.js.map +1 -0
  95. package/dist/esm/core/ABRController.js +454 -0
  96. package/dist/esm/core/ABRController.js.map +1 -0
  97. package/dist/esm/core/CodecUtils.js +193 -0
  98. package/dist/esm/core/CodecUtils.js.map +1 -0
  99. package/dist/esm/core/ErrorClassifier.js +408 -0
  100. package/dist/esm/core/ErrorClassifier.js.map +1 -0
  101. package/dist/esm/core/EventEmitter.js +106 -0
  102. package/dist/esm/core/EventEmitter.js.map +1 -0
  103. package/dist/esm/core/GatewayClient.js +340 -0
  104. package/dist/esm/core/GatewayClient.js.map +1 -0
  105. package/dist/esm/core/InteractionController.js +604 -0
  106. package/dist/esm/core/InteractionController.js.map +1 -0
  107. package/dist/esm/core/LiveDurationProxy.js +184 -0
  108. package/dist/esm/core/LiveDurationProxy.js.map +1 -0
  109. package/dist/esm/core/MetaTrackManager.js +622 -0
  110. package/dist/esm/core/MetaTrackManager.js.map +1 -0
  111. package/dist/esm/core/MistReporter.js +447 -0
  112. package/dist/esm/core/MistReporter.js.map +1 -0
  113. package/dist/esm/core/MistSignaling.js +262 -0
  114. package/dist/esm/core/MistSignaling.js.map +1 -0
  115. package/dist/esm/core/PlayerController.js +2651 -0
  116. package/dist/esm/core/PlayerController.js.map +1 -0
  117. package/dist/esm/core/PlayerInterface.js +267 -0
  118. package/dist/esm/core/PlayerInterface.js.map +1 -0
  119. package/dist/esm/core/PlayerManager.js +804 -0
  120. package/dist/esm/core/PlayerManager.js.map +1 -0
  121. package/dist/esm/core/PlayerRegistry.js +264 -0
  122. package/dist/esm/core/PlayerRegistry.js.map +1 -0
  123. package/dist/esm/core/QualityMonitor.js +471 -0
  124. package/dist/esm/core/QualityMonitor.js.map +1 -0
  125. package/dist/esm/core/SeekingUtils.js +280 -0
  126. package/dist/esm/core/SeekingUtils.js.map +1 -0
  127. package/dist/esm/core/StreamStateClient.js +379 -0
  128. package/dist/esm/core/StreamStateClient.js.map +1 -0
  129. package/dist/esm/core/SubtitleManager.js +225 -0
  130. package/dist/esm/core/SubtitleManager.js.map +1 -0
  131. package/dist/esm/core/TelemetryReporter.js +256 -0
  132. package/dist/esm/core/TelemetryReporter.js.map +1 -0
  133. package/dist/esm/core/TimeFormat.js +169 -0
  134. package/dist/esm/core/TimeFormat.js.map +1 -0
  135. package/dist/esm/core/TimerManager.js +174 -0
  136. package/dist/esm/core/TimerManager.js.map +1 -0
  137. package/dist/esm/core/UrlUtils.js +151 -0
  138. package/dist/esm/core/UrlUtils.js.map +1 -0
  139. package/dist/esm/core/detector.js +279 -0
  140. package/dist/esm/core/detector.js.map +1 -0
  141. package/dist/esm/core/scorer.js +422 -0
  142. package/dist/esm/core/scorer.js.map +1 -0
  143. package/dist/esm/index.js +26 -20043
  144. package/dist/esm/index.js.map +1 -1
  145. package/dist/esm/lib/utils.js +9 -0
  146. package/dist/esm/lib/utils.js.map +1 -0
  147. package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +4 -0
  148. package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
  149. package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3036 -0
  150. package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
  151. package/dist/esm/players/DashJsPlayer.js +636 -0
  152. package/dist/esm/players/DashJsPlayer.js.map +1 -0
  153. package/dist/esm/players/HlsJsPlayer.js +480 -0
  154. package/dist/esm/players/HlsJsPlayer.js.map +1 -0
  155. package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js +520 -0
  156. package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
  157. package/dist/esm/players/MewsWsPlayer/WebSocketManager.js +213 -0
  158. package/dist/esm/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
  159. package/dist/esm/players/MewsWsPlayer/index.js +985 -0
  160. package/dist/esm/players/MewsWsPlayer/index.js.map +1 -0
  161. package/dist/esm/players/MistPlayer.js +183 -0
  162. package/dist/esm/players/MistPlayer.js.map +1 -0
  163. package/dist/esm/players/MistWebRTCPlayer/index.js +633 -0
  164. package/dist/esm/players/MistWebRTCPlayer/index.js.map +1 -0
  165. package/dist/esm/players/NativePlayer.js +759 -0
  166. package/dist/esm/players/NativePlayer.js.map +1 -0
  167. package/dist/esm/players/VideoJsPlayer.js +583 -0
  168. package/dist/esm/players/VideoJsPlayer.js.map +1 -0
  169. package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js +233 -0
  170. package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
  171. package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js +134 -0
  172. package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
  173. package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js +91 -0
  174. package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
  175. package/dist/esm/players/WebCodecsPlayer/SyncController.js +357 -0
  176. package/dist/esm/players/WebCodecsPlayer/SyncController.js.map +1 -0
  177. package/dist/esm/players/WebCodecsPlayer/WebSocketController.js +458 -0
  178. package/dist/esm/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
  179. package/dist/esm/players/WebCodecsPlayer/index.js +1458 -0
  180. package/dist/esm/players/WebCodecsPlayer/index.js.map +1 -0
  181. package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +315 -0
  182. package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
  183. package/dist/esm/styles/index.js +54 -0
  184. package/dist/esm/styles/index.js.map +1 -0
  185. package/dist/esm/vanilla/FrameWorksPlayer.js +264 -0
  186. package/dist/esm/vanilla/FrameWorksPlayer.js.map +1 -0
  187. package/dist/esm/vanilla.js +2 -0
  188. package/dist/esm/vanilla.js.map +1 -0
  189. package/dist/player.css +4 -1
  190. package/dist/types/core/ABRController.d.ts +4 -4
  191. package/dist/types/core/CodecUtils.d.ts +1 -1
  192. package/dist/types/core/ErrorClassifier.d.ts +77 -0
  193. package/dist/types/core/GatewayClient.d.ts +4 -4
  194. package/dist/types/core/MetaTrackManager.d.ts +2 -2
  195. package/dist/types/core/MistReporter.d.ts +3 -3
  196. package/dist/types/core/MistSignaling.d.ts +12 -12
  197. package/dist/types/core/PlayerController.d.ts +19 -14
  198. package/dist/types/core/PlayerInterface.d.ts +100 -2
  199. package/dist/types/core/PlayerManager.d.ts +36 -9
  200. package/dist/types/core/PlayerRegistry.d.ts +11 -11
  201. package/dist/types/core/QualityMonitor.d.ts +2 -2
  202. package/dist/types/core/SeekingUtils.d.ts +2 -2
  203. package/dist/types/core/StreamStateClient.d.ts +2 -2
  204. package/dist/types/core/TelemetryReporter.d.ts +1 -1
  205. package/dist/types/core/TimerManager.d.ts +1 -1
  206. package/dist/types/core/detector.d.ts +1 -1
  207. package/dist/types/core/index.d.ts +44 -44
  208. package/dist/types/core/scorer.d.ts +1 -1
  209. package/dist/types/core/selector.d.ts +2 -2
  210. package/dist/types/index.d.ts +35 -34
  211. package/dist/types/players/DashJsPlayer.d.ts +3 -3
  212. package/dist/types/players/HlsJsPlayer.d.ts +3 -3
  213. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +1 -1
  214. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +1 -1
  215. package/dist/types/players/MewsWsPlayer/index.d.ts +2 -2
  216. package/dist/types/players/MewsWsPlayer/types.d.ts +15 -15
  217. package/dist/types/players/MistPlayer.d.ts +2 -2
  218. package/dist/types/players/MistWebRTCPlayer/index.d.ts +3 -3
  219. package/dist/types/players/NativePlayer.d.ts +3 -3
  220. package/dist/types/players/VideoJsPlayer.d.ts +3 -3
  221. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +3 -3
  222. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +1 -1
  223. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +2 -2
  224. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +2 -2
  225. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +3 -3
  226. package/dist/types/players/WebCodecsPlayer/index.d.ts +9 -9
  227. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +1 -1
  228. package/dist/types/players/WebCodecsPlayer/types.d.ts +49 -49
  229. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +31 -31
  230. package/dist/types/players/index.d.ts +5 -8
  231. package/dist/types/types.d.ts +15 -15
  232. package/dist/types/vanilla/FrameWorksPlayer.d.ts +2 -2
  233. package/dist/types/vanilla/index.d.ts +4 -4
  234. package/dist/workers/decoder.worker.js +129 -122
  235. package/dist/workers/decoder.worker.js.map +1 -1
  236. package/package.json +31 -15
  237. package/src/core/ErrorClassifier.ts +499 -0
  238. package/src/core/PlayerController.ts +17 -2
  239. package/src/core/PlayerInterface.ts +109 -0
  240. package/src/core/PlayerManager.ts +290 -46
  241. package/src/core/PlayerRegistry.ts +221 -87
  242. package/src/core/TelemetryReporter.ts +4 -1
  243. package/src/index.ts +13 -4
  244. package/src/players/WebCodecsPlayer/index.ts +2 -2
  245. package/src/players/index.ts +5 -16
  246. package/src/styles/player.css +4 -1
  247. 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