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