@livepeer-frameworks/player-svelte 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.
Files changed (169) hide show
  1. package/dist/DevModePanel.svelte +650 -0
  2. package/dist/DevModePanel.svelte.d.ts +31 -0
  3. package/dist/DvdLogo.svelte +213 -0
  4. package/dist/DvdLogo.svelte.d.ts +7 -0
  5. package/dist/Icons.svelte +27 -0
  6. package/dist/Icons.svelte.d.ts +25 -0
  7. package/dist/IdleScreen.svelte +752 -0
  8. package/dist/IdleScreen.svelte.d.ts +11 -0
  9. package/dist/LoadingScreen.svelte +689 -0
  10. package/dist/LoadingScreen.svelte.d.ts +7 -0
  11. package/dist/Player.svelte +482 -0
  12. package/dist/Player.svelte.d.ts +26 -0
  13. package/dist/PlayerControls.svelte +739 -0
  14. package/dist/PlayerControls.svelte.d.ts +20 -0
  15. package/dist/SeekBar.svelte +274 -0
  16. package/dist/SeekBar.svelte.d.ts +25 -0
  17. package/dist/SkipIndicator.svelte +95 -0
  18. package/dist/SkipIndicator.svelte.d.ts +14 -0
  19. package/dist/SpeedIndicator.svelte +38 -0
  20. package/dist/SpeedIndicator.svelte.d.ts +8 -0
  21. package/dist/StatsPanel.svelte +155 -0
  22. package/dist/StatsPanel.svelte.d.ts +27 -0
  23. package/dist/StreamStateOverlay.svelte +266 -0
  24. package/dist/StreamStateOverlay.svelte.d.ts +18 -0
  25. package/dist/SubtitleRenderer.svelte +234 -0
  26. package/dist/SubtitleRenderer.svelte.d.ts +41 -0
  27. package/dist/ThumbnailOverlay.svelte +96 -0
  28. package/dist/ThumbnailOverlay.svelte.d.ts +11 -0
  29. package/dist/TitleOverlay.svelte +47 -0
  30. package/dist/TitleOverlay.svelte.d.ts +9 -0
  31. package/dist/assets/logomark.svg +56 -0
  32. package/dist/components/VolumeIcons.svelte +53 -0
  33. package/dist/components/VolumeIcons.svelte.d.ts +10 -0
  34. package/dist/global.d.ts +15 -0
  35. package/dist/icons/FullscreenExitIcon.svelte +33 -0
  36. package/dist/icons/FullscreenExitIcon.svelte.d.ts +8 -0
  37. package/dist/icons/FullscreenIcon.svelte +33 -0
  38. package/dist/icons/FullscreenIcon.svelte.d.ts +8 -0
  39. package/dist/icons/PauseIcon.svelte +28 -0
  40. package/dist/icons/PauseIcon.svelte.d.ts +8 -0
  41. package/dist/icons/PictureInPictureIcon.svelte +28 -0
  42. package/dist/icons/PictureInPictureIcon.svelte.d.ts +8 -0
  43. package/dist/icons/PlayIcon.svelte +27 -0
  44. package/dist/icons/PlayIcon.svelte.d.ts +8 -0
  45. package/dist/icons/SeekToLiveIcon.svelte +30 -0
  46. package/dist/icons/SeekToLiveIcon.svelte.d.ts +8 -0
  47. package/dist/icons/SettingsIcon.svelte +40 -0
  48. package/dist/icons/SettingsIcon.svelte.d.ts +8 -0
  49. package/dist/icons/SkipBackIcon.svelte +32 -0
  50. package/dist/icons/SkipBackIcon.svelte.d.ts +8 -0
  51. package/dist/icons/SkipForwardIcon.svelte +32 -0
  52. package/dist/icons/SkipForwardIcon.svelte.d.ts +8 -0
  53. package/dist/icons/StatsIcon.svelte +29 -0
  54. package/dist/icons/StatsIcon.svelte.d.ts +8 -0
  55. package/dist/icons/VolumeOffIcon.svelte +29 -0
  56. package/dist/icons/VolumeOffIcon.svelte.d.ts +8 -0
  57. package/dist/icons/VolumeUpIcon.svelte +34 -0
  58. package/dist/icons/VolumeUpIcon.svelte.d.ts +8 -0
  59. package/dist/icons/index.d.ts +17 -0
  60. package/dist/icons/index.js +17 -0
  61. package/dist/index.d.ts +50 -0
  62. package/dist/index.js +54 -0
  63. package/dist/player.css +2 -0
  64. package/dist/stores/index.d.ts +15 -0
  65. package/dist/stores/index.js +21 -0
  66. package/dist/stores/playbackQuality.d.ts +43 -0
  67. package/dist/stores/playbackQuality.js +107 -0
  68. package/dist/stores/playerContext.d.ts +73 -0
  69. package/dist/stores/playerContext.js +166 -0
  70. package/dist/stores/playerController.d.ts +178 -0
  71. package/dist/stores/playerController.js +358 -0
  72. package/dist/stores/playerSelection.d.ts +84 -0
  73. package/dist/stores/playerSelection.js +159 -0
  74. package/dist/stores/streamState.d.ts +44 -0
  75. package/dist/stores/streamState.js +314 -0
  76. package/dist/stores/viewerEndpoints.d.ts +48 -0
  77. package/dist/stores/viewerEndpoints.js +178 -0
  78. package/dist/types.d.ts +4 -0
  79. package/dist/types.js +4 -0
  80. package/dist/ui/Badge.svelte +21 -0
  81. package/dist/ui/Badge.svelte.d.ts +32 -0
  82. package/dist/ui/Button.svelte +42 -0
  83. package/dist/ui/Button.svelte.d.ts +35 -0
  84. package/dist/ui/Slider.svelte +100 -0
  85. package/dist/ui/Slider.svelte.d.ts +17 -0
  86. package/dist/ui/badge.d.ts +6 -0
  87. package/dist/ui/badge.js +10 -0
  88. package/dist/ui/button.d.ts +8 -0
  89. package/dist/ui/button.js +21 -0
  90. package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
  91. package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +31 -0
  92. package/dist/ui/context-menu/ContextMenuContent.svelte +17 -0
  93. package/dist/ui/context-menu/ContextMenuContent.svelte.d.ts +7 -0
  94. package/dist/ui/context-menu/ContextMenuItem.svelte +22 -0
  95. package/dist/ui/context-menu/ContextMenuItem.svelte.d.ts +8 -0
  96. package/dist/ui/context-menu/ContextMenuLabel.svelte +22 -0
  97. package/dist/ui/context-menu/ContextMenuLabel.svelte.d.ts +8 -0
  98. package/dist/ui/context-menu/ContextMenuPortal.svelte +11 -0
  99. package/dist/ui/context-menu/ContextMenuPortal.svelte.d.ts +6 -0
  100. package/dist/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
  101. package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +31 -0
  102. package/dist/ui/context-menu/ContextMenuSeparator.svelte +14 -0
  103. package/dist/ui/context-menu/ContextMenuSeparator.svelte.d.ts +6 -0
  104. package/dist/ui/context-menu/ContextMenuShortcut.svelte +19 -0
  105. package/dist/ui/context-menu/ContextMenuShortcut.svelte.d.ts +7 -0
  106. package/dist/ui/context-menu/ContextMenuSubContent.svelte +20 -0
  107. package/dist/ui/context-menu/ContextMenuSubContent.svelte.d.ts +7 -0
  108. package/dist/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
  109. package/dist/ui/context-menu/ContextMenuSubTrigger.svelte.d.ts +8 -0
  110. package/dist/ui/context-menu/index.d.ts +17 -0
  111. package/dist/ui/context-menu/index.js +17 -0
  112. package/package.json +51 -0
  113. package/src/DevModePanel.svelte +650 -0
  114. package/src/DvdLogo.svelte +213 -0
  115. package/src/Icons.svelte +27 -0
  116. package/src/IdleScreen.svelte +739 -0
  117. package/src/LoadingScreen.svelte +674 -0
  118. package/src/Player.svelte +483 -0
  119. package/src/PlayerControls.svelte +752 -0
  120. package/src/SeekBar.svelte +274 -0
  121. package/src/SkipIndicator.svelte +95 -0
  122. package/src/SpeedIndicator.svelte +37 -0
  123. package/src/StatsPanel.svelte +155 -0
  124. package/src/StreamStateOverlay.svelte +266 -0
  125. package/src/SubtitleRenderer.svelte +234 -0
  126. package/src/ThumbnailOverlay.svelte +96 -0
  127. package/src/TitleOverlay.svelte +47 -0
  128. package/src/assets/logomark.svg +56 -0
  129. package/src/components/VolumeIcons.svelte +53 -0
  130. package/src/global.d.ts +15 -0
  131. package/src/icons/FullscreenExitIcon.svelte +33 -0
  132. package/src/icons/FullscreenIcon.svelte +33 -0
  133. package/src/icons/PauseIcon.svelte +28 -0
  134. package/src/icons/PictureInPictureIcon.svelte +28 -0
  135. package/src/icons/PlayIcon.svelte +27 -0
  136. package/src/icons/SeekToLiveIcon.svelte +30 -0
  137. package/src/icons/SettingsIcon.svelte +40 -0
  138. package/src/icons/SkipBackIcon.svelte +32 -0
  139. package/src/icons/SkipForwardIcon.svelte +32 -0
  140. package/src/icons/StatsIcon.svelte +29 -0
  141. package/src/icons/VolumeOffIcon.svelte +29 -0
  142. package/src/icons/VolumeUpIcon.svelte +34 -0
  143. package/src/icons/index.ts +18 -0
  144. package/src/index.ts +84 -0
  145. package/src/player.css +2 -0
  146. package/src/stores/index.ts +88 -0
  147. package/src/stores/playbackQuality.ts +137 -0
  148. package/src/stores/playerContext.ts +221 -0
  149. package/src/stores/playerController.ts +568 -0
  150. package/src/stores/playerSelection.ts +216 -0
  151. package/src/stores/streamState.ts +367 -0
  152. package/src/stores/viewerEndpoints.ts +224 -0
  153. package/src/types.ts +6 -0
  154. package/src/ui/Badge.svelte +21 -0
  155. package/src/ui/Button.svelte +42 -0
  156. package/src/ui/Slider.svelte +100 -0
  157. package/src/ui/badge.ts +20 -0
  158. package/src/ui/button.ts +35 -0
  159. package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
  160. package/src/ui/context-menu/ContextMenuContent.svelte +17 -0
  161. package/src/ui/context-menu/ContextMenuItem.svelte +22 -0
  162. package/src/ui/context-menu/ContextMenuLabel.svelte +22 -0
  163. package/src/ui/context-menu/ContextMenuPortal.svelte +11 -0
  164. package/src/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
  165. package/src/ui/context-menu/ContextMenuSeparator.svelte +14 -0
  166. package/src/ui/context-menu/ContextMenuShortcut.svelte +19 -0
  167. package/src/ui/context-menu/ContextMenuSubContent.svelte +20 -0
  168. package/src/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
  169. package/src/ui/context-menu/index.ts +36 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Svelte store for player selection.
3
+ *
4
+ * Subscribes to PlayerManager events for reactive selection updates.
5
+ * Uses event-driven updates instead of polling - no render spam.
6
+ */
7
+
8
+ import { writable, derived, type Readable } from 'svelte/store';
9
+ import {
10
+ type PlayerManager,
11
+ type PlayerSelection,
12
+ type PlayerCombination,
13
+ type StreamInfo,
14
+ type PlaybackMode,
15
+ } from '@livepeer-frameworks/player-core';
16
+
17
+ export interface PlayerSelectionOptions {
18
+ /** Enable debug logging */
19
+ debug?: boolean;
20
+ }
21
+
22
+ export interface PlayerSelectionState {
23
+ /** Current best selection (null if no compatible player) */
24
+ selection: PlayerSelection | null;
25
+ /** All player+source combinations with scores */
26
+ combinations: PlayerCombination[];
27
+ /** Whether initial computation has completed */
28
+ ready: boolean;
29
+ }
30
+
31
+ export interface PlayerSelectionStore extends Readable<PlayerSelectionState> {
32
+ /** Update stream info to compute selections for */
33
+ setStreamInfo: (streamInfo: StreamInfo | null, playbackMode?: PlaybackMode) => void;
34
+ /** Force recomputation (invalidates cache) */
35
+ refresh: () => void;
36
+ /** Cleanup subscriptions */
37
+ destroy: () => void;
38
+ }
39
+
40
+ const initialState: PlayerSelectionState = {
41
+ selection: null,
42
+ combinations: [],
43
+ ready: false,
44
+ };
45
+
46
+ /**
47
+ * Create a player selection store that subscribes to PlayerManager events.
48
+ *
49
+ * This store uses the event system in PlayerManager, which means:
50
+ * - Initial computation happens once when streamInfo is provided
51
+ * - Updates only fire when selection actually changes (different player+source)
52
+ * - No render spam from frequent reactive updates
53
+ *
54
+ * @example
55
+ * ```svelte
56
+ * <script>
57
+ * import { createPlayerSelectionStore } from './stores/playerSelection';
58
+ * import { globalPlayerManager } from '@livepeer-frameworks/player-core';
59
+ *
60
+ * const playerSelection = createPlayerSelectionStore(globalPlayerManager);
61
+ *
62
+ * // Set stream info to trigger selection
63
+ * $: if (streamInfo) playerSelection.setStreamInfo(streamInfo, playbackMode);
64
+ *
65
+ * // Access selection state
66
+ * $: selection = $playerSelection.selection;
67
+ * $: combinations = $playerSelection.combinations;
68
+ * $: ready = $playerSelection.ready;
69
+ * </script>
70
+ * ```
71
+ */
72
+ export function createPlayerSelectionStore(
73
+ manager: PlayerManager,
74
+ options: PlayerSelectionOptions = {}
75
+ ): PlayerSelectionStore {
76
+ const { debug = false } = options;
77
+
78
+ const store = writable<PlayerSelectionState>({ ...initialState });
79
+
80
+ let currentStreamInfo: StreamInfo | null = null;
81
+ let currentPlaybackMode: PlaybackMode = 'auto';
82
+ let unsubSelection: (() => void) | null = null;
83
+ let unsubCombos: (() => void) | null = null;
84
+
85
+ // Subscribe to PlayerManager events
86
+ function subscribe() {
87
+ // Clean up existing subscriptions
88
+ unsubscribe();
89
+
90
+ unsubSelection = manager.on('selection-changed', (sel) => {
91
+ if (debug) {
92
+ console.log('[playerSelection store] Selection changed:', sel?.player, sel?.source?.type);
93
+ }
94
+ store.update((state) => ({
95
+ ...state,
96
+ selection: sel,
97
+ }));
98
+ });
99
+
100
+ unsubCombos = manager.on('combinations-updated', (combos) => {
101
+ if (debug) {
102
+ console.log('[playerSelection store] Combinations updated:', combos.length);
103
+ }
104
+ store.update((state) => ({
105
+ ...state,
106
+ combinations: combos,
107
+ ready: true,
108
+ }));
109
+ });
110
+ }
111
+
112
+ function unsubscribe() {
113
+ unsubSelection?.();
114
+ unsubCombos?.();
115
+ unsubSelection = null;
116
+ unsubCombos = null;
117
+ }
118
+
119
+ // Initialize subscriptions
120
+ subscribe();
121
+
122
+ /**
123
+ * Set stream info to compute selections for.
124
+ * This triggers computation (using cache if available).
125
+ */
126
+ function setStreamInfo(streamInfo: StreamInfo | null, playbackMode?: PlaybackMode) {
127
+ currentStreamInfo = streamInfo;
128
+ currentPlaybackMode = playbackMode ?? 'auto';
129
+
130
+ if (!streamInfo) {
131
+ store.set({ ...initialState });
132
+ return;
133
+ }
134
+
135
+ // This will use cache if available, or compute + emit events if not
136
+ manager.getAllCombinations(streamInfo, currentPlaybackMode);
137
+ }
138
+
139
+ /**
140
+ * Force recomputation (invalidates cache).
141
+ */
142
+ function refresh() {
143
+ if (!currentStreamInfo) return;
144
+ manager.invalidateCache();
145
+ manager.getAllCombinations(currentStreamInfo, currentPlaybackMode);
146
+ }
147
+
148
+ /**
149
+ * Cleanup subscriptions.
150
+ */
151
+ function destroy() {
152
+ unsubscribe();
153
+ store.set({ ...initialState });
154
+ currentStreamInfo = null;
155
+ }
156
+
157
+ return {
158
+ subscribe: store.subscribe,
159
+ setStreamInfo,
160
+ refresh,
161
+ destroy,
162
+ };
163
+ }
164
+
165
+ // Convenience derived stores
166
+
167
+ /**
168
+ * Derive just the current selection from the store.
169
+ */
170
+ export function createDerivedSelection(store: PlayerSelectionStore): Readable<PlayerSelection | null> {
171
+ return derived(store, ($state) => $state.selection);
172
+ }
173
+
174
+ /**
175
+ * Derive just the combinations array from the store.
176
+ */
177
+ export function createDerivedCombinations(store: PlayerSelectionStore): Readable<PlayerCombination[]> {
178
+ return derived(store, ($state) => $state.combinations);
179
+ }
180
+
181
+ /**
182
+ * Derive the ready state from the store.
183
+ */
184
+ export function createDerivedReady(store: PlayerSelectionStore): Readable<boolean> {
185
+ return derived(store, ($state) => $state.ready);
186
+ }
187
+
188
+ /**
189
+ * Derive the selected player name.
190
+ */
191
+ export function createDerivedSelectedPlayer(store: PlayerSelectionStore): Readable<string | null> {
192
+ return derived(store, ($state) => $state.selection?.player ?? null);
193
+ }
194
+
195
+ /**
196
+ * Derive the selected source type.
197
+ */
198
+ export function createDerivedSelectedSourceType(store: PlayerSelectionStore): Readable<string | null> {
199
+ return derived(store, ($state) => $state.selection?.source?.type ?? null);
200
+ }
201
+
202
+ /**
203
+ * Derive only compatible combinations (filtered from all).
204
+ */
205
+ export function createDerivedCompatibleCombinations(store: PlayerSelectionStore): Readable<PlayerCombination[]> {
206
+ return derived(store, ($state) => $state.combinations.filter((c) => c.compatible));
207
+ }
208
+
209
+ /**
210
+ * Derive only incompatible combinations.
211
+ */
212
+ export function createDerivedIncompatibleCombinations(store: PlayerSelectionStore): Readable<PlayerCombination[]> {
213
+ return derived(store, ($state) => $state.combinations.filter((c) => !c.compatible));
214
+ }
215
+
216
+ export default createPlayerSelectionStore;
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Svelte store for MistServer stream state polling via WebSocket/HTTP.
3
+ *
4
+ * Port of useStreamState.ts React hook to Svelte 5 stores.
5
+ */
6
+
7
+ import { writable, derived, type Readable } from 'svelte/store';
8
+ import type { StreamStatus, MistStreamInfo, StreamState } from '@livepeer-frameworks/player-core';
9
+
10
+ export interface StreamStateOptions {
11
+ mistBaseUrl: string;
12
+ streamName: string;
13
+ pollInterval?: number;
14
+ enabled?: boolean;
15
+ useWebSocket?: boolean;
16
+ }
17
+
18
+ export interface StreamStateStore extends Readable<StreamState> {
19
+ refetch: () => void;
20
+ getSocket: () => WebSocket | null;
21
+ isSocketReady: Readable<boolean>;
22
+ destroy: () => void;
23
+ }
24
+
25
+ /**
26
+ * Parse MistServer error string into StreamStatus enum
27
+ */
28
+ function parseErrorToStatus(error: string): StreamStatus {
29
+ const lowerError = error.toLowerCase();
30
+
31
+ if (lowerError.includes('offline')) return 'OFFLINE';
32
+ if (lowerError.includes('initializing')) return 'INITIALIZING';
33
+ if (lowerError.includes('booting')) return 'BOOTING';
34
+ if (lowerError.includes('waiting for data')) return 'WAITING_FOR_DATA';
35
+ if (lowerError.includes('shutting down')) return 'SHUTTING_DOWN';
36
+ if (lowerError.includes('invalid')) return 'INVALID';
37
+
38
+ return 'ERROR';
39
+ }
40
+
41
+ /**
42
+ * Get human-readable message for stream status
43
+ */
44
+ function getStatusMessage(status: StreamStatus, percentage?: number): string {
45
+ switch (status) {
46
+ case 'ONLINE':
47
+ return 'Stream is online';
48
+ case 'OFFLINE':
49
+ return 'Stream is offline';
50
+ case 'INITIALIZING':
51
+ return percentage !== undefined
52
+ ? `Initializing... ${Math.round(percentage * 10) / 10}%`
53
+ : 'Stream is initializing';
54
+ case 'BOOTING':
55
+ return 'Stream is starting up';
56
+ case 'WAITING_FOR_DATA':
57
+ return 'Waiting for stream data';
58
+ case 'SHUTTING_DOWN':
59
+ return 'Stream is shutting down';
60
+ case 'INVALID':
61
+ return 'Stream status is invalid';
62
+ case 'ERROR':
63
+ default:
64
+ return 'Stream error';
65
+ }
66
+ }
67
+
68
+ const initialState: StreamState = {
69
+ status: 'OFFLINE',
70
+ isOnline: false,
71
+ message: 'Connecting...',
72
+ lastUpdate: 0,
73
+ };
74
+
75
+ /**
76
+ * Create a stream state manager store for MistServer polling.
77
+ *
78
+ * @example
79
+ * ```svelte
80
+ * <script>
81
+ * import { createStreamStateManager } from './stores/streamState';
82
+ *
83
+ * const streamState = createStreamStateManager({
84
+ * mistBaseUrl: 'https://mist.example.com',
85
+ * streamName: 'my-stream',
86
+ * });
87
+ *
88
+ * // Access values
89
+ * $: status = $streamState.status;
90
+ * $: isOnline = $streamState.isOnline;
91
+ * </script>
92
+ * ```
93
+ */
94
+ export function createStreamStateManager(options: StreamStateOptions): StreamStateStore {
95
+ const {
96
+ mistBaseUrl,
97
+ streamName,
98
+ pollInterval = 3000,
99
+ enabled = true,
100
+ useWebSocket = true,
101
+ } = options;
102
+
103
+ const WS_TIMEOUT_MS = 5000;
104
+
105
+ // Internal state
106
+ const store = writable<StreamState>(initialState);
107
+ const socketReady = writable(false);
108
+ let ws: WebSocket | null = null;
109
+ let pollTimeout: ReturnType<typeof setTimeout> | null = null;
110
+ let wsTimeout: ReturnType<typeof setTimeout> | null = null;
111
+ let mounted = true;
112
+
113
+ /**
114
+ * Process MistServer response data
115
+ */
116
+ function processStreamInfo(data: MistStreamInfo) {
117
+ if (!mounted) return;
118
+
119
+ if (data.error) {
120
+ const status = parseErrorToStatus(data.error);
121
+ const message = data.on_error || getStatusMessage(status, data.perc);
122
+
123
+ store.update(prev => ({
124
+ status,
125
+ isOnline: false,
126
+ message,
127
+ percentage: data.perc,
128
+ lastUpdate: Date.now(),
129
+ error: data.error,
130
+ streamInfo: prev.streamInfo, // Preserve track data through error states
131
+ }));
132
+ } else {
133
+ // Stream is online with valid metadata
134
+ store.update(prev => {
135
+ const mergedStreamInfo: MistStreamInfo = {
136
+ ...prev.streamInfo,
137
+ ...data,
138
+ source: data.source || prev.streamInfo?.source,
139
+ meta: {
140
+ ...prev.streamInfo?.meta,
141
+ ...data.meta,
142
+ tracks: data.meta?.tracks || prev.streamInfo?.meta?.tracks,
143
+ },
144
+ };
145
+
146
+ return {
147
+ status: 'ONLINE',
148
+ isOnline: true,
149
+ message: 'Stream is online',
150
+ lastUpdate: Date.now(),
151
+ streamInfo: mergedStreamInfo,
152
+ };
153
+ });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * HTTP polling fallback
159
+ */
160
+ async function pollHttp() {
161
+ if (!mounted || !enabled) return;
162
+
163
+ try {
164
+ const baseUrl = `${mistBaseUrl.replace(/\/$/, '')}/json_${encodeURIComponent(streamName)}.js`;
165
+ const url = `${baseUrl}?metaeverywhere=1&inclzero=1`;
166
+ const response = await fetch(url, {
167
+ method: 'GET',
168
+ headers: { 'Accept': 'application/json' },
169
+ });
170
+
171
+ if (!response.ok) {
172
+ throw new Error(`HTTP ${response.status}`);
173
+ }
174
+
175
+ let text = await response.text();
176
+ // Strip JSONP callback if present
177
+ const jsonpMatch = text.match(/^[^(]+\(([\s\S]*)\);?$/);
178
+ if (jsonpMatch) {
179
+ text = jsonpMatch[1];
180
+ }
181
+
182
+ const data = JSON.parse(text) as MistStreamInfo;
183
+ processStreamInfo(data);
184
+ } catch (error) {
185
+ if (!mounted) return;
186
+
187
+ store.update(prev => ({
188
+ ...prev,
189
+ status: 'ERROR',
190
+ isOnline: false,
191
+ message: error instanceof Error ? error.message : 'Connection failed',
192
+ lastUpdate: Date.now(),
193
+ error: error instanceof Error ? error.message : 'Unknown error',
194
+ }));
195
+ }
196
+
197
+ // Schedule next poll
198
+ if (mounted && enabled && !useWebSocket) {
199
+ pollTimeout = setTimeout(pollHttp, pollInterval);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * WebSocket connection with timeout fallback
205
+ */
206
+ function connectWebSocket() {
207
+ if (!mounted || !enabled || !useWebSocket) return;
208
+
209
+ // Clean up existing
210
+ if (wsTimeout) {
211
+ clearTimeout(wsTimeout);
212
+ wsTimeout = null;
213
+ }
214
+ if (ws) {
215
+ ws.close();
216
+ ws = null;
217
+ }
218
+
219
+ try {
220
+ const wsUrl = mistBaseUrl
221
+ .replace(/^http:/, 'ws:')
222
+ .replace(/^https:/, 'wss:')
223
+ .replace(/\/$/, '');
224
+
225
+ const url = `${wsUrl}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`;
226
+ const socket = new WebSocket(url);
227
+ ws = socket;
228
+
229
+ // Timeout: if no message within 5 seconds, fall back to HTTP
230
+ wsTimeout = setTimeout(() => {
231
+ if (socket.readyState <= WebSocket.OPEN) {
232
+ console.debug('[streamState] WebSocket timeout (5s), falling back to HTTP');
233
+ socket.close();
234
+ pollHttp();
235
+ }
236
+ }, WS_TIMEOUT_MS);
237
+
238
+ socket.onopen = () => {
239
+ console.debug('[streamState] WebSocket connected');
240
+ socketReady.set(true);
241
+ };
242
+
243
+ socket.onmessage = (event) => {
244
+ if (wsTimeout) {
245
+ clearTimeout(wsTimeout);
246
+ wsTimeout = null;
247
+ }
248
+
249
+ try {
250
+ const data = JSON.parse(event.data) as MistStreamInfo;
251
+ processStreamInfo(data);
252
+ } catch (e) {
253
+ console.warn('[streamState] Failed to parse WebSocket message:', e);
254
+ }
255
+ };
256
+
257
+ socket.onerror = () => {
258
+ console.warn('[streamState] WebSocket error, falling back to HTTP');
259
+ if (wsTimeout) {
260
+ clearTimeout(wsTimeout);
261
+ wsTimeout = null;
262
+ }
263
+ socket.close();
264
+ };
265
+
266
+ socket.onclose = () => {
267
+ ws = null;
268
+ socketReady.set(false);
269
+
270
+ if (!mounted || !enabled) return;
271
+
272
+ console.debug('[streamState] WebSocket closed, starting HTTP polling');
273
+ pollHttp();
274
+ };
275
+ } catch (error) {
276
+ console.warn('[streamState] WebSocket connection failed:', error);
277
+ pollHttp();
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Manual refetch
283
+ */
284
+ function refetch() {
285
+ if (useWebSocket && ws?.readyState === WebSocket.OPEN) {
286
+ return; // WebSocket will receive updates automatically
287
+ }
288
+ pollHttp();
289
+ }
290
+
291
+ /**
292
+ * Get current WebSocket reference
293
+ */
294
+ function getSocket(): WebSocket | null {
295
+ return ws;
296
+ }
297
+
298
+ /**
299
+ * Cleanup and destroy the store
300
+ */
301
+ function destroy() {
302
+ mounted = false;
303
+
304
+ if (wsTimeout) {
305
+ clearTimeout(wsTimeout);
306
+ wsTimeout = null;
307
+ }
308
+
309
+ if (ws) {
310
+ ws.onclose = null;
311
+ ws.onerror = null;
312
+ ws.onmessage = null;
313
+ ws.onopen = null;
314
+ ws.close();
315
+ ws = null;
316
+ }
317
+
318
+ if (pollTimeout) {
319
+ clearTimeout(pollTimeout);
320
+ pollTimeout = null;
321
+ }
322
+
323
+ socketReady.set(false);
324
+ store.set(initialState);
325
+ }
326
+
327
+ // Initialize connection
328
+ if (enabled && mistBaseUrl && streamName) {
329
+ store.set({
330
+ ...initialState,
331
+ message: 'Connecting...',
332
+ lastUpdate: Date.now(),
333
+ });
334
+
335
+ // Initial HTTP poll then WebSocket
336
+ const init = async () => {
337
+ await pollHttp();
338
+ if (useWebSocket && mounted) {
339
+ connectWebSocket();
340
+ }
341
+ };
342
+ init();
343
+ }
344
+
345
+ return {
346
+ subscribe: store.subscribe,
347
+ refetch,
348
+ getSocket,
349
+ isSocketReady: { subscribe: socketReady.subscribe },
350
+ destroy,
351
+ };
352
+ }
353
+
354
+ // Convenience derived stores for common values
355
+ export function createDerivedStreamStatus(store: StreamStateStore) {
356
+ return derived(store, $state => $state.status);
357
+ }
358
+
359
+ export function createDerivedIsOnline(store: StreamStateStore) {
360
+ return derived(store, $state => $state.isOnline);
361
+ }
362
+
363
+ export function createDerivedStreamInfo(store: StreamStateStore) {
364
+ return derived(store, $state => $state.streamInfo);
365
+ }
366
+
367
+ export default createStreamStateManager;