@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.
- package/dist/DevModePanel.svelte +650 -0
- package/dist/DevModePanel.svelte.d.ts +31 -0
- package/dist/DvdLogo.svelte +213 -0
- package/dist/DvdLogo.svelte.d.ts +7 -0
- package/dist/Icons.svelte +27 -0
- package/dist/Icons.svelte.d.ts +25 -0
- package/dist/IdleScreen.svelte +752 -0
- package/dist/IdleScreen.svelte.d.ts +11 -0
- package/dist/LoadingScreen.svelte +689 -0
- package/dist/LoadingScreen.svelte.d.ts +7 -0
- package/dist/Player.svelte +482 -0
- package/dist/Player.svelte.d.ts +26 -0
- package/dist/PlayerControls.svelte +739 -0
- package/dist/PlayerControls.svelte.d.ts +20 -0
- package/dist/SeekBar.svelte +274 -0
- package/dist/SeekBar.svelte.d.ts +25 -0
- package/dist/SkipIndicator.svelte +95 -0
- package/dist/SkipIndicator.svelte.d.ts +14 -0
- package/dist/SpeedIndicator.svelte +38 -0
- package/dist/SpeedIndicator.svelte.d.ts +8 -0
- package/dist/StatsPanel.svelte +155 -0
- package/dist/StatsPanel.svelte.d.ts +27 -0
- package/dist/StreamStateOverlay.svelte +266 -0
- package/dist/StreamStateOverlay.svelte.d.ts +18 -0
- package/dist/SubtitleRenderer.svelte +234 -0
- package/dist/SubtitleRenderer.svelte.d.ts +41 -0
- package/dist/ThumbnailOverlay.svelte +96 -0
- package/dist/ThumbnailOverlay.svelte.d.ts +11 -0
- package/dist/TitleOverlay.svelte +47 -0
- package/dist/TitleOverlay.svelte.d.ts +9 -0
- package/dist/assets/logomark.svg +56 -0
- package/dist/components/VolumeIcons.svelte +53 -0
- package/dist/components/VolumeIcons.svelte.d.ts +10 -0
- package/dist/global.d.ts +15 -0
- package/dist/icons/FullscreenExitIcon.svelte +33 -0
- package/dist/icons/FullscreenExitIcon.svelte.d.ts +8 -0
- package/dist/icons/FullscreenIcon.svelte +33 -0
- package/dist/icons/FullscreenIcon.svelte.d.ts +8 -0
- package/dist/icons/PauseIcon.svelte +28 -0
- package/dist/icons/PauseIcon.svelte.d.ts +8 -0
- package/dist/icons/PictureInPictureIcon.svelte +28 -0
- package/dist/icons/PictureInPictureIcon.svelte.d.ts +8 -0
- package/dist/icons/PlayIcon.svelte +27 -0
- package/dist/icons/PlayIcon.svelte.d.ts +8 -0
- package/dist/icons/SeekToLiveIcon.svelte +30 -0
- package/dist/icons/SeekToLiveIcon.svelte.d.ts +8 -0
- package/dist/icons/SettingsIcon.svelte +40 -0
- package/dist/icons/SettingsIcon.svelte.d.ts +8 -0
- package/dist/icons/SkipBackIcon.svelte +32 -0
- package/dist/icons/SkipBackIcon.svelte.d.ts +8 -0
- package/dist/icons/SkipForwardIcon.svelte +32 -0
- package/dist/icons/SkipForwardIcon.svelte.d.ts +8 -0
- package/dist/icons/StatsIcon.svelte +29 -0
- package/dist/icons/StatsIcon.svelte.d.ts +8 -0
- package/dist/icons/VolumeOffIcon.svelte +29 -0
- package/dist/icons/VolumeOffIcon.svelte.d.ts +8 -0
- package/dist/icons/VolumeUpIcon.svelte +34 -0
- package/dist/icons/VolumeUpIcon.svelte.d.ts +8 -0
- package/dist/icons/index.d.ts +17 -0
- package/dist/icons/index.js +17 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +54 -0
- package/dist/player.css +2 -0
- package/dist/stores/index.d.ts +15 -0
- package/dist/stores/index.js +21 -0
- package/dist/stores/playbackQuality.d.ts +43 -0
- package/dist/stores/playbackQuality.js +107 -0
- package/dist/stores/playerContext.d.ts +73 -0
- package/dist/stores/playerContext.js +166 -0
- package/dist/stores/playerController.d.ts +178 -0
- package/dist/stores/playerController.js +358 -0
- package/dist/stores/playerSelection.d.ts +84 -0
- package/dist/stores/playerSelection.js +159 -0
- package/dist/stores/streamState.d.ts +44 -0
- package/dist/stores/streamState.js +314 -0
- package/dist/stores/viewerEndpoints.d.ts +48 -0
- package/dist/stores/viewerEndpoints.js +178 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +4 -0
- package/dist/ui/Badge.svelte +21 -0
- package/dist/ui/Badge.svelte.d.ts +32 -0
- package/dist/ui/Button.svelte +42 -0
- package/dist/ui/Button.svelte.d.ts +35 -0
- package/dist/ui/Slider.svelte +100 -0
- package/dist/ui/Slider.svelte.d.ts +17 -0
- package/dist/ui/badge.d.ts +6 -0
- package/dist/ui/badge.js +10 -0
- package/dist/ui/button.d.ts +8 -0
- package/dist/ui/button.js +21 -0
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +31 -0
- package/dist/ui/context-menu/ContextMenuContent.svelte +17 -0
- package/dist/ui/context-menu/ContextMenuContent.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuItem.svelte +22 -0
- package/dist/ui/context-menu/ContextMenuItem.svelte.d.ts +8 -0
- package/dist/ui/context-menu/ContextMenuLabel.svelte +22 -0
- package/dist/ui/context-menu/ContextMenuLabel.svelte.d.ts +8 -0
- package/dist/ui/context-menu/ContextMenuPortal.svelte +11 -0
- package/dist/ui/context-menu/ContextMenuPortal.svelte.d.ts +6 -0
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +31 -0
- package/dist/ui/context-menu/ContextMenuSeparator.svelte +14 -0
- package/dist/ui/context-menu/ContextMenuSeparator.svelte.d.ts +6 -0
- package/dist/ui/context-menu/ContextMenuShortcut.svelte +19 -0
- package/dist/ui/context-menu/ContextMenuShortcut.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuSubContent.svelte +20 -0
- package/dist/ui/context-menu/ContextMenuSubContent.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
- package/dist/ui/context-menu/ContextMenuSubTrigger.svelte.d.ts +8 -0
- package/dist/ui/context-menu/index.d.ts +17 -0
- package/dist/ui/context-menu/index.js +17 -0
- package/package.json +51 -0
- package/src/DevModePanel.svelte +650 -0
- package/src/DvdLogo.svelte +213 -0
- package/src/Icons.svelte +27 -0
- package/src/IdleScreen.svelte +739 -0
- package/src/LoadingScreen.svelte +674 -0
- package/src/Player.svelte +483 -0
- package/src/PlayerControls.svelte +752 -0
- package/src/SeekBar.svelte +274 -0
- package/src/SkipIndicator.svelte +95 -0
- package/src/SpeedIndicator.svelte +37 -0
- package/src/StatsPanel.svelte +155 -0
- package/src/StreamStateOverlay.svelte +266 -0
- package/src/SubtitleRenderer.svelte +234 -0
- package/src/ThumbnailOverlay.svelte +96 -0
- package/src/TitleOverlay.svelte +47 -0
- package/src/assets/logomark.svg +56 -0
- package/src/components/VolumeIcons.svelte +53 -0
- package/src/global.d.ts +15 -0
- package/src/icons/FullscreenExitIcon.svelte +33 -0
- package/src/icons/FullscreenIcon.svelte +33 -0
- package/src/icons/PauseIcon.svelte +28 -0
- package/src/icons/PictureInPictureIcon.svelte +28 -0
- package/src/icons/PlayIcon.svelte +27 -0
- package/src/icons/SeekToLiveIcon.svelte +30 -0
- package/src/icons/SettingsIcon.svelte +40 -0
- package/src/icons/SkipBackIcon.svelte +32 -0
- package/src/icons/SkipForwardIcon.svelte +32 -0
- package/src/icons/StatsIcon.svelte +29 -0
- package/src/icons/VolumeOffIcon.svelte +29 -0
- package/src/icons/VolumeUpIcon.svelte +34 -0
- package/src/icons/index.ts +18 -0
- package/src/index.ts +84 -0
- package/src/player.css +2 -0
- package/src/stores/index.ts +88 -0
- package/src/stores/playbackQuality.ts +137 -0
- package/src/stores/playerContext.ts +221 -0
- package/src/stores/playerController.ts +568 -0
- package/src/stores/playerSelection.ts +216 -0
- package/src/stores/streamState.ts +367 -0
- package/src/stores/viewerEndpoints.ts +224 -0
- package/src/types.ts +6 -0
- package/src/ui/Badge.svelte +21 -0
- package/src/ui/Button.svelte +42 -0
- package/src/ui/Slider.svelte +100 -0
- package/src/ui/badge.ts +20 -0
- package/src/ui/button.ts +35 -0
- package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
- package/src/ui/context-menu/ContextMenuContent.svelte +17 -0
- package/src/ui/context-menu/ContextMenuItem.svelte +22 -0
- package/src/ui/context-menu/ContextMenuLabel.svelte +22 -0
- package/src/ui/context-menu/ContextMenuPortal.svelte +11 -0
- package/src/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
- package/src/ui/context-menu/ContextMenuSeparator.svelte +14 -0
- package/src/ui/context-menu/ContextMenuShortcut.svelte +19 -0
- package/src/ui/context-menu/ContextMenuSubContent.svelte +20 -0
- package/src/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
- 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;
|