@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,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorClassifier - Centralized error classification and recovery orchestration
|
|
3
|
+
*
|
|
4
|
+
* Implements a 4-tier error handling system:
|
|
5
|
+
* - Tier 1 (TRANSIENT): Silent retry with exponential backoff
|
|
6
|
+
* - Tier 2 (RECOVERABLE): Protocol/player swap with toast notification
|
|
7
|
+
* - Tier 3 (DEGRADED): Quality drop with informational toast
|
|
8
|
+
* - Tier 4 (FATAL): Blocking error modal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ErrorSeverity,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
type ClassifiedError,
|
|
15
|
+
type ErrorHandlingEvents,
|
|
16
|
+
} from "./PlayerInterface";
|
|
17
|
+
|
|
18
|
+
/** Retry configuration for each error code */
|
|
19
|
+
interface RetryConfig {
|
|
20
|
+
maxAttempts: number;
|
|
21
|
+
baseDelayMs: number;
|
|
22
|
+
maxDelayMs: number;
|
|
23
|
+
jitterPercent: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Default retry configurations by error code */
|
|
27
|
+
const RETRY_CONFIGS: Record<ErrorCode, RetryConfig> = {
|
|
28
|
+
// Tier 1: Silent recovery
|
|
29
|
+
[ErrorCode.NETWORK_TIMEOUT]: {
|
|
30
|
+
maxAttempts: 3,
|
|
31
|
+
baseDelayMs: 500,
|
|
32
|
+
maxDelayMs: 4000,
|
|
33
|
+
jitterPercent: 20,
|
|
34
|
+
},
|
|
35
|
+
[ErrorCode.WEBSOCKET_DISCONNECT]: {
|
|
36
|
+
maxAttempts: 5,
|
|
37
|
+
baseDelayMs: 500,
|
|
38
|
+
maxDelayMs: 5000,
|
|
39
|
+
jitterPercent: 20,
|
|
40
|
+
},
|
|
41
|
+
[ErrorCode.SEGMENT_LOAD_ERROR]: {
|
|
42
|
+
maxAttempts: 3,
|
|
43
|
+
baseDelayMs: 200,
|
|
44
|
+
maxDelayMs: 2000,
|
|
45
|
+
jitterPercent: 10,
|
|
46
|
+
},
|
|
47
|
+
[ErrorCode.ICE_DISCONNECTED]: {
|
|
48
|
+
maxAttempts: 3,
|
|
49
|
+
baseDelayMs: 500,
|
|
50
|
+
maxDelayMs: 3000,
|
|
51
|
+
jitterPercent: 20,
|
|
52
|
+
},
|
|
53
|
+
[ErrorCode.BUFFER_UNDERRUN]: {
|
|
54
|
+
maxAttempts: 1,
|
|
55
|
+
baseDelayMs: 5000,
|
|
56
|
+
maxDelayMs: 5000,
|
|
57
|
+
jitterPercent: 0,
|
|
58
|
+
},
|
|
59
|
+
[ErrorCode.CODEC_DECODE_ERROR]: {
|
|
60
|
+
maxAttempts: 3,
|
|
61
|
+
baseDelayMs: 100,
|
|
62
|
+
maxDelayMs: 1000,
|
|
63
|
+
jitterPercent: 10,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Tier 2: Protocol swap (no internal retry, just count for tracking)
|
|
67
|
+
[ErrorCode.PROTOCOL_UNSUPPORTED]: {
|
|
68
|
+
maxAttempts: 1,
|
|
69
|
+
baseDelayMs: 0,
|
|
70
|
+
maxDelayMs: 0,
|
|
71
|
+
jitterPercent: 0,
|
|
72
|
+
},
|
|
73
|
+
[ErrorCode.CODEC_INCOMPATIBLE]: {
|
|
74
|
+
maxAttempts: 1,
|
|
75
|
+
baseDelayMs: 0,
|
|
76
|
+
maxDelayMs: 0,
|
|
77
|
+
jitterPercent: 0,
|
|
78
|
+
},
|
|
79
|
+
[ErrorCode.ICE_FAILED]: { maxAttempts: 1, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
80
|
+
[ErrorCode.MANIFEST_STALE]: { maxAttempts: 1, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
81
|
+
[ErrorCode.PLAYER_INIT_FAILED]: {
|
|
82
|
+
maxAttempts: 1,
|
|
83
|
+
baseDelayMs: 0,
|
|
84
|
+
maxDelayMs: 0,
|
|
85
|
+
jitterPercent: 0,
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Tier 3: Quality (not retried, just tracked)
|
|
89
|
+
[ErrorCode.QUALITY_DROPPED]: { maxAttempts: 0, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
90
|
+
[ErrorCode.BANDWIDTH_LIMITED]: {
|
|
91
|
+
maxAttempts: 0,
|
|
92
|
+
baseDelayMs: 0,
|
|
93
|
+
maxDelayMs: 0,
|
|
94
|
+
jitterPercent: 0,
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Tier 4: Fatal (not retried)
|
|
98
|
+
[ErrorCode.ALL_PROTOCOLS_EXHAUSTED]: {
|
|
99
|
+
maxAttempts: 0,
|
|
100
|
+
baseDelayMs: 0,
|
|
101
|
+
maxDelayMs: 0,
|
|
102
|
+
jitterPercent: 0,
|
|
103
|
+
},
|
|
104
|
+
[ErrorCode.ALL_PROTOCOLS_BLACKLISTED]: {
|
|
105
|
+
maxAttempts: 0,
|
|
106
|
+
baseDelayMs: 0,
|
|
107
|
+
maxDelayMs: 0,
|
|
108
|
+
jitterPercent: 0,
|
|
109
|
+
},
|
|
110
|
+
[ErrorCode.STREAM_OFFLINE]: { maxAttempts: 0, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
111
|
+
[ErrorCode.AUTH_REQUIRED]: { maxAttempts: 0, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
112
|
+
[ErrorCode.GEO_BLOCKED]: { maxAttempts: 0, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
113
|
+
[ErrorCode.DRM_ERROR]: { maxAttempts: 0, baseDelayMs: 0, maxDelayMs: 0, jitterPercent: 0 },
|
|
114
|
+
[ErrorCode.CONTENT_UNAVAILABLE]: {
|
|
115
|
+
maxAttempts: 0,
|
|
116
|
+
baseDelayMs: 0,
|
|
117
|
+
maxDelayMs: 0,
|
|
118
|
+
jitterPercent: 0,
|
|
119
|
+
},
|
|
120
|
+
[ErrorCode.UNKNOWN]: { maxAttempts: 1, baseDelayMs: 1000, maxDelayMs: 1000, jitterPercent: 0 },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Maps error codes to their default severity */
|
|
124
|
+
const CODE_TO_SEVERITY: Record<ErrorCode, ErrorSeverity> = {
|
|
125
|
+
// Tier 1
|
|
126
|
+
[ErrorCode.NETWORK_TIMEOUT]: ErrorSeverity.TRANSIENT,
|
|
127
|
+
[ErrorCode.WEBSOCKET_DISCONNECT]: ErrorSeverity.TRANSIENT,
|
|
128
|
+
[ErrorCode.SEGMENT_LOAD_ERROR]: ErrorSeverity.TRANSIENT,
|
|
129
|
+
[ErrorCode.ICE_DISCONNECTED]: ErrorSeverity.TRANSIENT,
|
|
130
|
+
[ErrorCode.BUFFER_UNDERRUN]: ErrorSeverity.TRANSIENT,
|
|
131
|
+
[ErrorCode.CODEC_DECODE_ERROR]: ErrorSeverity.TRANSIENT,
|
|
132
|
+
|
|
133
|
+
// Tier 2
|
|
134
|
+
[ErrorCode.PROTOCOL_UNSUPPORTED]: ErrorSeverity.RECOVERABLE,
|
|
135
|
+
[ErrorCode.CODEC_INCOMPATIBLE]: ErrorSeverity.RECOVERABLE,
|
|
136
|
+
[ErrorCode.ICE_FAILED]: ErrorSeverity.RECOVERABLE,
|
|
137
|
+
[ErrorCode.MANIFEST_STALE]: ErrorSeverity.RECOVERABLE,
|
|
138
|
+
[ErrorCode.PLAYER_INIT_FAILED]: ErrorSeverity.RECOVERABLE,
|
|
139
|
+
|
|
140
|
+
// Tier 3
|
|
141
|
+
[ErrorCode.QUALITY_DROPPED]: ErrorSeverity.DEGRADED,
|
|
142
|
+
[ErrorCode.BANDWIDTH_LIMITED]: ErrorSeverity.DEGRADED,
|
|
143
|
+
|
|
144
|
+
// Tier 4
|
|
145
|
+
[ErrorCode.ALL_PROTOCOLS_EXHAUSTED]: ErrorSeverity.FATAL,
|
|
146
|
+
[ErrorCode.ALL_PROTOCOLS_BLACKLISTED]: ErrorSeverity.FATAL,
|
|
147
|
+
[ErrorCode.STREAM_OFFLINE]: ErrorSeverity.FATAL,
|
|
148
|
+
[ErrorCode.AUTH_REQUIRED]: ErrorSeverity.FATAL,
|
|
149
|
+
[ErrorCode.GEO_BLOCKED]: ErrorSeverity.FATAL,
|
|
150
|
+
[ErrorCode.DRM_ERROR]: ErrorSeverity.FATAL,
|
|
151
|
+
[ErrorCode.CONTENT_UNAVAILABLE]: ErrorSeverity.FATAL,
|
|
152
|
+
[ErrorCode.UNKNOWN]: ErrorSeverity.FATAL,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** User-friendly messages for each error code */
|
|
156
|
+
const CODE_TO_MESSAGE: Record<ErrorCode, string> = {
|
|
157
|
+
[ErrorCode.NETWORK_TIMEOUT]: "Network timeout",
|
|
158
|
+
[ErrorCode.WEBSOCKET_DISCONNECT]: "Connection lost",
|
|
159
|
+
[ErrorCode.SEGMENT_LOAD_ERROR]: "Failed to load video segment",
|
|
160
|
+
[ErrorCode.ICE_DISCONNECTED]: "Connection interrupted",
|
|
161
|
+
[ErrorCode.BUFFER_UNDERRUN]: "Buffering",
|
|
162
|
+
[ErrorCode.CODEC_DECODE_ERROR]: "Decode error",
|
|
163
|
+
[ErrorCode.PROTOCOL_UNSUPPORTED]: "Protocol not supported",
|
|
164
|
+
[ErrorCode.CODEC_INCOMPATIBLE]: "Codec not supported",
|
|
165
|
+
[ErrorCode.ICE_FAILED]: "Connection failed",
|
|
166
|
+
[ErrorCode.MANIFEST_STALE]: "Stream manifest outdated",
|
|
167
|
+
[ErrorCode.PLAYER_INIT_FAILED]: "Player initialization failed",
|
|
168
|
+
[ErrorCode.QUALITY_DROPPED]: "Quality reduced",
|
|
169
|
+
[ErrorCode.BANDWIDTH_LIMITED]: "Bandwidth limited",
|
|
170
|
+
[ErrorCode.ALL_PROTOCOLS_EXHAUSTED]: "Unable to play video",
|
|
171
|
+
[ErrorCode.ALL_PROTOCOLS_BLACKLISTED]: "No compatible playback protocols available",
|
|
172
|
+
[ErrorCode.STREAM_OFFLINE]: "Stream is offline",
|
|
173
|
+
[ErrorCode.AUTH_REQUIRED]: "Sign in to watch",
|
|
174
|
+
[ErrorCode.GEO_BLOCKED]: "Not available in your region",
|
|
175
|
+
[ErrorCode.DRM_ERROR]: "Playback not supported",
|
|
176
|
+
[ErrorCode.CONTENT_UNAVAILABLE]: "Content unavailable",
|
|
177
|
+
[ErrorCode.UNKNOWN]: "Playback error",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
type EventListener<K extends keyof ErrorHandlingEvents> = (data: ErrorHandlingEvents[K]) => void;
|
|
181
|
+
|
|
182
|
+
export type RecoveryAction =
|
|
183
|
+
| { type: "retry"; delayMs: number }
|
|
184
|
+
| { type: "swap"; reason: string }
|
|
185
|
+
| { type: "toast"; message: string }
|
|
186
|
+
| { type: "fatal"; error: ClassifiedError };
|
|
187
|
+
|
|
188
|
+
export interface ErrorClassifierOptions {
|
|
189
|
+
/** Number of alternative player/protocol combos available */
|
|
190
|
+
alternativesCount?: number;
|
|
191
|
+
/** Enable debug logging */
|
|
192
|
+
debug?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Centralized error classifier that tracks retry state and determines recovery actions.
|
|
197
|
+
*/
|
|
198
|
+
export class ErrorClassifier {
|
|
199
|
+
private retryCounts: Map<ErrorCode, number> = new Map();
|
|
200
|
+
private lastErrorTime: Map<ErrorCode, number> = new Map();
|
|
201
|
+
private alternativesRemaining: number;
|
|
202
|
+
private listeners: Map<string, Set<Function>> = new Map();
|
|
203
|
+
private debug: boolean;
|
|
204
|
+
|
|
205
|
+
// Debounce tracking for quality toasts
|
|
206
|
+
private lastQualityToastTime = 0;
|
|
207
|
+
private static readonly QUALITY_TOAST_DEBOUNCE_MS = 10000;
|
|
208
|
+
|
|
209
|
+
constructor(options: ErrorClassifierOptions = {}) {
|
|
210
|
+
this.alternativesRemaining = options.alternativesCount ?? 0;
|
|
211
|
+
this.debug = options.debug ?? false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Update the count of remaining alternatives (called after a swap attempt)
|
|
216
|
+
*/
|
|
217
|
+
setAlternativesRemaining(count: number): void {
|
|
218
|
+
this.alternativesRemaining = count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reset retry counts (call when playback successfully resumes)
|
|
223
|
+
*/
|
|
224
|
+
reset(): void {
|
|
225
|
+
this.retryCounts.clear();
|
|
226
|
+
this.lastErrorTime.clear();
|
|
227
|
+
this.lastQualityToastTime = 0;
|
|
228
|
+
this.log("Error state reset");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Classify a raw error and determine the appropriate recovery action.
|
|
233
|
+
*
|
|
234
|
+
* @param code - Error code identifying the error type
|
|
235
|
+
* @param originalError - Original error object or message
|
|
236
|
+
* @returns The recovery action to take
|
|
237
|
+
*/
|
|
238
|
+
classify(code: ErrorCode, originalError?: Error | string): RecoveryAction {
|
|
239
|
+
const config = RETRY_CONFIGS[code];
|
|
240
|
+
const currentAttempt = (this.retryCounts.get(code) ?? 0) + 1;
|
|
241
|
+
const retriesRemaining = Math.max(0, config.maxAttempts - currentAttempt);
|
|
242
|
+
|
|
243
|
+
// Update retry count
|
|
244
|
+
this.retryCounts.set(code, currentAttempt);
|
|
245
|
+
this.lastErrorTime.set(code, Date.now());
|
|
246
|
+
|
|
247
|
+
const classified: ClassifiedError = {
|
|
248
|
+
severity: CODE_TO_SEVERITY[code],
|
|
249
|
+
code,
|
|
250
|
+
message: CODE_TO_MESSAGE[code],
|
|
251
|
+
retriesRemaining,
|
|
252
|
+
alternativesRemaining: this.alternativesRemaining,
|
|
253
|
+
originalError,
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
this.log(
|
|
258
|
+
`Classified error: ${code}, attempt ${currentAttempt}/${config.maxAttempts}, severity ${classified.severity}`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Tier 1: Silent retry if retries remaining
|
|
262
|
+
if (classified.severity === ErrorSeverity.TRANSIENT && retriesRemaining > 0) {
|
|
263
|
+
const delayMs = this.calculateBackoff(code, currentAttempt);
|
|
264
|
+
this.emit("recoveryAttempted", {
|
|
265
|
+
code,
|
|
266
|
+
attempt: currentAttempt,
|
|
267
|
+
maxAttempts: config.maxAttempts,
|
|
268
|
+
});
|
|
269
|
+
return { type: "retry", delayMs };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Tier 1 exhausted or Tier 2: Try protocol swap if alternatives exist
|
|
273
|
+
if (
|
|
274
|
+
(classified.severity === ErrorSeverity.TRANSIENT && retriesRemaining === 0) ||
|
|
275
|
+
classified.severity === ErrorSeverity.RECOVERABLE
|
|
276
|
+
) {
|
|
277
|
+
if (this.alternativesRemaining > 0) {
|
|
278
|
+
return { type: "swap", reason: classified.message };
|
|
279
|
+
}
|
|
280
|
+
// No alternatives: escalate to fatal
|
|
281
|
+
const originalCode = classified.code;
|
|
282
|
+
const originalMessage = classified.message;
|
|
283
|
+
classified.severity = ErrorSeverity.FATAL;
|
|
284
|
+
classified.code = ErrorCode.ALL_PROTOCOLS_EXHAUSTED;
|
|
285
|
+
classified.message = `${originalMessage} (no alternatives remaining)`;
|
|
286
|
+
classified.details = {
|
|
287
|
+
...classified.details,
|
|
288
|
+
originalCode,
|
|
289
|
+
originalMessage,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Tier 3: Quality degradation toast (debounced)
|
|
294
|
+
if (classified.severity === ErrorSeverity.DEGRADED) {
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
if (now - this.lastQualityToastTime >= ErrorClassifier.QUALITY_TOAST_DEBOUNCE_MS) {
|
|
297
|
+
this.lastQualityToastTime = now;
|
|
298
|
+
this.emit("qualityChanged", {
|
|
299
|
+
direction: "down",
|
|
300
|
+
reason: classified.message,
|
|
301
|
+
});
|
|
302
|
+
return { type: "toast", message: classified.message };
|
|
303
|
+
}
|
|
304
|
+
// Debounced: no action needed
|
|
305
|
+
return { type: "toast", message: "" };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Tier 4: Fatal error
|
|
309
|
+
this.emit("playbackFailed", classified);
|
|
310
|
+
return { type: "fatal", error: classified };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
classifyWithDetails(
|
|
314
|
+
code: ErrorCode,
|
|
315
|
+
message: string,
|
|
316
|
+
details?: ClassifiedError["details"],
|
|
317
|
+
originalError?: Error | string
|
|
318
|
+
): RecoveryAction {
|
|
319
|
+
if (CODE_TO_SEVERITY[code] === ErrorSeverity.FATAL) {
|
|
320
|
+
const classified: ClassifiedError = {
|
|
321
|
+
severity: ErrorSeverity.FATAL,
|
|
322
|
+
code,
|
|
323
|
+
message,
|
|
324
|
+
retriesRemaining: 0,
|
|
325
|
+
alternativesRemaining: this.alternativesRemaining,
|
|
326
|
+
originalError,
|
|
327
|
+
timestamp: Date.now(),
|
|
328
|
+
details,
|
|
329
|
+
};
|
|
330
|
+
this.emit("playbackFailed", classified);
|
|
331
|
+
return { type: "fatal", error: classified };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const action = this.classify(code, originalError);
|
|
335
|
+
if (action.type === "fatal") {
|
|
336
|
+
action.error.message = message;
|
|
337
|
+
action.error.details = details;
|
|
338
|
+
}
|
|
339
|
+
return action;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Notify classifier that a protocol swap occurred (for event emission)
|
|
344
|
+
*/
|
|
345
|
+
notifyProtocolSwap(
|
|
346
|
+
fromPlayer: string,
|
|
347
|
+
toPlayer: string,
|
|
348
|
+
fromProtocol: string,
|
|
349
|
+
toProtocol: string,
|
|
350
|
+
reason: string
|
|
351
|
+
): void {
|
|
352
|
+
// Note: alternativesRemaining is managed by PlayerManager.setAlternativesRemaining()
|
|
353
|
+
// Don't decrement here to avoid double-counting
|
|
354
|
+
this.emit("protocolSwapped", {
|
|
355
|
+
fromPlayer,
|
|
356
|
+
toPlayer,
|
|
357
|
+
fromProtocol,
|
|
358
|
+
toProtocol,
|
|
359
|
+
reason,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Calculate exponential backoff delay with jitter
|
|
365
|
+
*/
|
|
366
|
+
private calculateBackoff(code: ErrorCode, attempt: number): number {
|
|
367
|
+
const config = RETRY_CONFIGS[code];
|
|
368
|
+
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt - 1);
|
|
369
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
370
|
+
|
|
371
|
+
// Add jitter
|
|
372
|
+
const jitterRange = cappedDelay * (config.jitterPercent / 100);
|
|
373
|
+
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
374
|
+
|
|
375
|
+
return Math.round(cappedDelay + jitter);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Map common error patterns to error codes
|
|
380
|
+
*/
|
|
381
|
+
static mapErrorToCode(error: Error | string): ErrorCode {
|
|
382
|
+
const message = typeof error === "string" ? error : error.message;
|
|
383
|
+
const lowerMessage = message.toLowerCase();
|
|
384
|
+
|
|
385
|
+
// Network errors
|
|
386
|
+
if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
|
|
387
|
+
return ErrorCode.NETWORK_TIMEOUT;
|
|
388
|
+
}
|
|
389
|
+
if (lowerMessage.includes("websocket") || lowerMessage.includes("socket")) {
|
|
390
|
+
return ErrorCode.WEBSOCKET_DISCONNECT;
|
|
391
|
+
}
|
|
392
|
+
if (lowerMessage.includes("fetch") || lowerMessage.includes("network")) {
|
|
393
|
+
return ErrorCode.NETWORK_TIMEOUT;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Stream state - check before segment errors (404 can mean offline)
|
|
397
|
+
if (
|
|
398
|
+
lowerMessage.includes("offline") ||
|
|
399
|
+
lowerMessage.includes("not found") ||
|
|
400
|
+
lowerMessage.includes("stream not found")
|
|
401
|
+
) {
|
|
402
|
+
return ErrorCode.STREAM_OFFLINE;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Segment/manifest errors (only if not a stream-level 404)
|
|
406
|
+
if (lowerMessage.includes("segment")) {
|
|
407
|
+
return ErrorCode.SEGMENT_LOAD_ERROR;
|
|
408
|
+
}
|
|
409
|
+
if (lowerMessage.includes("manifest") || lowerMessage.includes("playlist")) {
|
|
410
|
+
// Manifest 404 = stream offline, not stale
|
|
411
|
+
if (lowerMessage.includes("404")) {
|
|
412
|
+
return ErrorCode.STREAM_OFFLINE;
|
|
413
|
+
}
|
|
414
|
+
return ErrorCode.MANIFEST_STALE;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ICE/WebRTC errors
|
|
418
|
+
if (lowerMessage.includes("ice") && lowerMessage.includes("disconnect")) {
|
|
419
|
+
return ErrorCode.ICE_DISCONNECTED;
|
|
420
|
+
}
|
|
421
|
+
if (lowerMessage.includes("ice") && lowerMessage.includes("fail")) {
|
|
422
|
+
return ErrorCode.ICE_FAILED;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Codec errors
|
|
426
|
+
if (lowerMessage.includes("codec") || lowerMessage.includes("decode")) {
|
|
427
|
+
return ErrorCode.CODEC_DECODE_ERROR;
|
|
428
|
+
}
|
|
429
|
+
if (lowerMessage.includes("not supported") || lowerMessage.includes("unsupported")) {
|
|
430
|
+
return ErrorCode.PROTOCOL_UNSUPPORTED;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Buffer errors
|
|
434
|
+
if (lowerMessage.includes("buffer") || lowerMessage.includes("underrun")) {
|
|
435
|
+
return ErrorCode.BUFFER_UNDERRUN;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Auth errors
|
|
439
|
+
if (
|
|
440
|
+
lowerMessage.includes("401") ||
|
|
441
|
+
lowerMessage.includes("auth") ||
|
|
442
|
+
lowerMessage.includes("unauthorized")
|
|
443
|
+
) {
|
|
444
|
+
return ErrorCode.AUTH_REQUIRED;
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
lowerMessage.includes("403") ||
|
|
448
|
+
lowerMessage.includes("forbidden") ||
|
|
449
|
+
lowerMessage.includes("geo")
|
|
450
|
+
) {
|
|
451
|
+
return ErrorCode.GEO_BLOCKED;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// DRM
|
|
455
|
+
if (
|
|
456
|
+
lowerMessage.includes("drm") ||
|
|
457
|
+
lowerMessage.includes("eme") ||
|
|
458
|
+
lowerMessage.includes("key")
|
|
459
|
+
) {
|
|
460
|
+
return ErrorCode.DRM_ERROR;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return ErrorCode.UNKNOWN;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Event emitter methods
|
|
467
|
+
on<K extends keyof ErrorHandlingEvents>(event: K, listener: EventListener<K>): void {
|
|
468
|
+
if (!this.listeners.has(event)) {
|
|
469
|
+
this.listeners.set(event, new Set());
|
|
470
|
+
}
|
|
471
|
+
this.listeners.get(event)!.add(listener);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
off<K extends keyof ErrorHandlingEvents>(event: K, listener: EventListener<K>): void {
|
|
475
|
+
const eventListeners = this.listeners.get(event);
|
|
476
|
+
if (eventListeners) {
|
|
477
|
+
eventListeners.delete(listener);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private emit<K extends keyof ErrorHandlingEvents>(event: K, data: ErrorHandlingEvents[K]): void {
|
|
482
|
+
const eventListeners = this.listeners.get(event);
|
|
483
|
+
if (eventListeners) {
|
|
484
|
+
eventListeners.forEach((listener) => {
|
|
485
|
+
try {
|
|
486
|
+
(listener as EventListener<K>)(data);
|
|
487
|
+
} catch (e) {
|
|
488
|
+
console.error(`Error in ${event} listener:`, e);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private log(message: string): void {
|
|
495
|
+
if (this.debug) {
|
|
496
|
+
console.log(`[ErrorClassifier] ${message}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { TypedEventEmitter } from "./EventEmitter";
|
|
12
12
|
import { GatewayClient } from "./GatewayClient";
|
|
13
13
|
import { StreamStateClient } from "./StreamStateClient";
|
|
14
|
-
import { PlayerManager } from "./PlayerManager";
|
|
14
|
+
import type { PlayerManager, PlayerManagerEvents } from "./PlayerManager";
|
|
15
15
|
import { globalPlayerManager, ensurePlayersRegistered } from "./PlayerRegistry";
|
|
16
16
|
import { ABRController } from "./ABRController";
|
|
17
17
|
import { InteractionController } from "./InteractionController";
|
|
@@ -186,6 +186,15 @@ export interface PlayerControllerEvents {
|
|
|
186
186
|
volume: number;
|
|
187
187
|
muted: boolean;
|
|
188
188
|
};
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Error Handling Events (from PlayerManager)
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
/** Protocol/player swap occurred - show toast */
|
|
195
|
+
protocolSwapped: PlayerManagerEvents["protocolSwapped"];
|
|
196
|
+
/** Playback failed after all recovery attempts - show error modal */
|
|
197
|
+
playbackFailed: PlayerManagerEvents["playbackFailed"];
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
// ============================================================================
|
|
@@ -642,6 +651,10 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
642
651
|
this.config = config;
|
|
643
652
|
this.playerManager = config.playerManager || globalPlayerManager;
|
|
644
653
|
|
|
654
|
+
// Forward error handling events from PlayerManager
|
|
655
|
+
this.playerManager.on("protocolSwapped", (data) => this.emit("protocolSwapped", data));
|
|
656
|
+
this.playerManager.on("playbackFailed", (data) => this.emit("playbackFailed", data));
|
|
657
|
+
|
|
645
658
|
// Load loop state from localStorage
|
|
646
659
|
try {
|
|
647
660
|
if (typeof localStorage !== "undefined") {
|
|
@@ -2252,7 +2265,9 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2252
2265
|
this.setState("gateway_loading", { gatewayStatus: "loading" });
|
|
2253
2266
|
|
|
2254
2267
|
try {
|
|
2255
|
-
|
|
2268
|
+
let baseUrl = mistUrl;
|
|
2269
|
+
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
2270
|
+
const jsonUrl = `${baseUrl}/json_${encodeURIComponent(contentId)}.js`;
|
|
2256
2271
|
this.log(`[resolveFromMistServer] Fetching ${jsonUrl}`);
|
|
2257
2272
|
|
|
2258
2273
|
const response = await fetch(jsonUrl, { cache: "no-store" });
|
|
@@ -93,6 +93,115 @@ export interface PlayerEvents {
|
|
|
93
93
|
seekablechange: { start: number; end: number; bufferWindow: number };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Error severity levels for the tiered error handling system.
|
|
98
|
+
*
|
|
99
|
+
* Tier 1 (TRANSIENT): Silent retry, no UI - network timeouts, brief stalls
|
|
100
|
+
* Tier 2 (RECOVERABLE): Protocol swap with toast - alternatives exist
|
|
101
|
+
* Tier 3 (DEGRADED): Quality drop with toast - playback continues at lower quality
|
|
102
|
+
* Tier 4 (FATAL): Blocking modal - all options exhausted
|
|
103
|
+
*/
|
|
104
|
+
export enum ErrorSeverity {
|
|
105
|
+
/** Transient issues that self-resolve. User never sees UI. */
|
|
106
|
+
TRANSIENT = 1,
|
|
107
|
+
/** Current protocol failed but alternatives exist. Shows toast on swap. */
|
|
108
|
+
RECOVERABLE = 2,
|
|
109
|
+
/** Quality degraded but playback continues. Shows informational toast. */
|
|
110
|
+
DEGRADED = 3,
|
|
111
|
+
/** Cannot continue playback. Shows blocking error modal. */
|
|
112
|
+
FATAL = 4,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Error codes for classification. Maps to specific recovery strategies.
|
|
117
|
+
*/
|
|
118
|
+
export enum ErrorCode {
|
|
119
|
+
// Tier 1: Silent recovery
|
|
120
|
+
NETWORK_TIMEOUT = "NETWORK_TIMEOUT",
|
|
121
|
+
WEBSOCKET_DISCONNECT = "WEBSOCKET_DISCONNECT",
|
|
122
|
+
SEGMENT_LOAD_ERROR = "SEGMENT_LOAD_ERROR",
|
|
123
|
+
ICE_DISCONNECTED = "ICE_DISCONNECTED",
|
|
124
|
+
BUFFER_UNDERRUN = "BUFFER_UNDERRUN",
|
|
125
|
+
CODEC_DECODE_ERROR = "CODEC_DECODE_ERROR",
|
|
126
|
+
|
|
127
|
+
// Tier 2: Protocol swap
|
|
128
|
+
PROTOCOL_UNSUPPORTED = "PROTOCOL_UNSUPPORTED",
|
|
129
|
+
CODEC_INCOMPATIBLE = "CODEC_INCOMPATIBLE",
|
|
130
|
+
ICE_FAILED = "ICE_FAILED",
|
|
131
|
+
MANIFEST_STALE = "MANIFEST_STALE",
|
|
132
|
+
PLAYER_INIT_FAILED = "PLAYER_INIT_FAILED",
|
|
133
|
+
|
|
134
|
+
// Tier 3: Quality degraded
|
|
135
|
+
QUALITY_DROPPED = "QUALITY_DROPPED",
|
|
136
|
+
BANDWIDTH_LIMITED = "BANDWIDTH_LIMITED",
|
|
137
|
+
|
|
138
|
+
// Tier 4: Fatal
|
|
139
|
+
ALL_PROTOCOLS_EXHAUSTED = "ALL_PROTOCOLS_EXHAUSTED",
|
|
140
|
+
ALL_PROTOCOLS_BLACKLISTED = "ALL_PROTOCOLS_BLACKLISTED",
|
|
141
|
+
STREAM_OFFLINE = "STREAM_OFFLINE",
|
|
142
|
+
AUTH_REQUIRED = "AUTH_REQUIRED",
|
|
143
|
+
GEO_BLOCKED = "GEO_BLOCKED",
|
|
144
|
+
DRM_ERROR = "DRM_ERROR",
|
|
145
|
+
CONTENT_UNAVAILABLE = "CONTENT_UNAVAILABLE",
|
|
146
|
+
UNKNOWN = "UNKNOWN",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Classified error with severity and recovery metadata.
|
|
151
|
+
* Used by ErrorClassifier to track retry state and decide next action.
|
|
152
|
+
*/
|
|
153
|
+
export interface ClassifiedError {
|
|
154
|
+
/** Severity tier determining UI behavior */
|
|
155
|
+
severity: ErrorSeverity;
|
|
156
|
+
/** Specific error code for recovery strategy lookup */
|
|
157
|
+
code: ErrorCode;
|
|
158
|
+
/** Human-readable error message */
|
|
159
|
+
message: string;
|
|
160
|
+
/** Number of retries remaining for this error type */
|
|
161
|
+
retriesRemaining: number;
|
|
162
|
+
/** Number of alternative protocols/players remaining */
|
|
163
|
+
alternativesRemaining: number;
|
|
164
|
+
/** Original error if available */
|
|
165
|
+
originalError?: Error | string;
|
|
166
|
+
/** Timestamp when error occurred */
|
|
167
|
+
timestamp: number;
|
|
168
|
+
/** Diagnostic details for operators/debugging */
|
|
169
|
+
details?: {
|
|
170
|
+
incompatibilityReasons?: string[];
|
|
171
|
+
blacklistedProtocols?: string[];
|
|
172
|
+
originalCode?: ErrorCode;
|
|
173
|
+
originalMessage?: string;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Events emitted by the error handling system.
|
|
179
|
+
* UI layers listen to these for toast/modal display.
|
|
180
|
+
*/
|
|
181
|
+
export interface ErrorHandlingEvents {
|
|
182
|
+
/** Silent recovery attempted (Tier 1) - for telemetry only */
|
|
183
|
+
recoveryAttempted: {
|
|
184
|
+
code: ErrorCode;
|
|
185
|
+
attempt: number;
|
|
186
|
+
maxAttempts: number;
|
|
187
|
+
};
|
|
188
|
+
/** Protocol or player swapped (Tier 2) - shows toast */
|
|
189
|
+
protocolSwapped: {
|
|
190
|
+
fromPlayer: string;
|
|
191
|
+
toPlayer: string;
|
|
192
|
+
fromProtocol: string;
|
|
193
|
+
toProtocol: string;
|
|
194
|
+
reason: string;
|
|
195
|
+
};
|
|
196
|
+
/** Quality changed (Tier 3) - shows toast */
|
|
197
|
+
qualityChanged: {
|
|
198
|
+
direction: "up" | "down";
|
|
199
|
+
reason: string;
|
|
200
|
+
};
|
|
201
|
+
/** All recovery options exhausted (Tier 4) - shows modal */
|
|
202
|
+
playbackFailed: ClassifiedError;
|
|
203
|
+
}
|
|
204
|
+
|
|
96
205
|
/**
|
|
97
206
|
* Base interface all players must implement
|
|
98
207
|
*/
|