@livepeer-frameworks/player-core 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +19493 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +19398 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/player.css +2140 -0
- package/dist/types/core/ABRController.d.ts +164 -0
- package/dist/types/core/CodecUtils.d.ts +54 -0
- package/dist/types/core/Disposable.d.ts +61 -0
- package/dist/types/core/EventEmitter.d.ts +73 -0
- package/dist/types/core/GatewayClient.d.ts +144 -0
- package/dist/types/core/InteractionController.d.ts +121 -0
- package/dist/types/core/LiveDurationProxy.d.ts +102 -0
- package/dist/types/core/MetaTrackManager.d.ts +220 -0
- package/dist/types/core/MistReporter.d.ts +163 -0
- package/dist/types/core/MistSignaling.d.ts +148 -0
- package/dist/types/core/PlayerController.d.ts +665 -0
- package/dist/types/core/PlayerInterface.d.ts +230 -0
- package/dist/types/core/PlayerManager.d.ts +182 -0
- package/dist/types/core/PlayerRegistry.d.ts +27 -0
- package/dist/types/core/QualityMonitor.d.ts +184 -0
- package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
- package/dist/types/core/SeekingUtils.d.ts +142 -0
- package/dist/types/core/StreamStateClient.d.ts +108 -0
- package/dist/types/core/SubtitleManager.d.ts +111 -0
- package/dist/types/core/TelemetryReporter.d.ts +79 -0
- package/dist/types/core/TimeFormat.d.ts +97 -0
- package/dist/types/core/TimerManager.d.ts +83 -0
- package/dist/types/core/UrlUtils.d.ts +81 -0
- package/dist/types/core/detector.d.ts +149 -0
- package/dist/types/core/index.d.ts +49 -0
- package/dist/types/core/scorer.d.ts +167 -0
- package/dist/types/core/selector.d.ts +9 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/players/DashJsPlayer.d.ts +102 -0
- package/dist/types/players/HlsJsPlayer.d.ts +70 -0
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
- package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
- package/dist/types/players/MistPlayer.d.ts +25 -0
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
- package/dist/types/players/NativePlayer.d.ts +143 -0
- package/dist/types/players/VideoJsPlayer.d.ts +59 -0
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
- package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
- package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
- package/dist/types/players/index.d.ts +14 -0
- package/dist/types/styles/index.d.ts +11 -0
- package/dist/types/types.d.ts +363 -0
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
- package/dist/types/vanilla/index.d.ts +19 -0
- package/dist/workers/decoder.worker.js +989 -0
- package/dist/workers/decoder.worker.js.map +1 -0
- package/package.json +80 -0
- package/src/core/ABRController.ts +550 -0
- package/src/core/CodecUtils.ts +257 -0
- package/src/core/Disposable.ts +120 -0
- package/src/core/EventEmitter.ts +113 -0
- package/src/core/GatewayClient.ts +439 -0
- package/src/core/InteractionController.ts +712 -0
- package/src/core/LiveDurationProxy.ts +270 -0
- package/src/core/MetaTrackManager.ts +753 -0
- package/src/core/MistReporter.ts +543 -0
- package/src/core/MistSignaling.ts +346 -0
- package/src/core/PlayerController.ts +2829 -0
- package/src/core/PlayerInterface.ts +432 -0
- package/src/core/PlayerManager.ts +900 -0
- package/src/core/PlayerRegistry.ts +149 -0
- package/src/core/QualityMonitor.ts +597 -0
- package/src/core/ScreenWakeLockManager.ts +163 -0
- package/src/core/SeekingUtils.ts +364 -0
- package/src/core/StreamStateClient.ts +457 -0
- package/src/core/SubtitleManager.ts +297 -0
- package/src/core/TelemetryReporter.ts +308 -0
- package/src/core/TimeFormat.ts +205 -0
- package/src/core/TimerManager.ts +209 -0
- package/src/core/UrlUtils.ts +179 -0
- package/src/core/detector.ts +382 -0
- package/src/core/index.ts +140 -0
- package/src/core/scorer.ts +553 -0
- package/src/core/selector.ts +16 -0
- package/src/global.d.ts +11 -0
- package/src/index.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/players/DashJsPlayer.ts +642 -0
- package/src/players/HlsJsPlayer.ts +483 -0
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
- package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
- package/src/players/MewsWsPlayer/index.ts +1065 -0
- package/src/players/MewsWsPlayer/types.ts +106 -0
- package/src/players/MistPlayer.ts +188 -0
- package/src/players/MistWebRTCPlayer/index.ts +703 -0
- package/src/players/NativePlayer.ts +820 -0
- package/src/players/VideoJsPlayer.ts +643 -0
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
- package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
- package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
- package/src/players/WebCodecsPlayer/index.ts +1650 -0
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
- package/src/players/WebCodecsPlayer/types.ts +542 -0
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
- package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
- package/src/players/index.ts +22 -0
- package/src/styles/animations.css +21 -0
- package/src/styles/index.ts +52 -0
- package/src/styles/player.css +2126 -0
- package/src/styles/tailwind.css +1015 -0
- package/src/types.ts +421 -0
- package/src/vanilla/FrameWorksPlayer.ts +367 -0
- package/src/vanilla/index.ts +22 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeFormat.ts
|
|
3
|
+
*
|
|
4
|
+
* Time formatting utilities for player controls.
|
|
5
|
+
* Used by React, Svelte, and Vanilla wrappers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface TimeDisplayParams {
|
|
13
|
+
isLive: boolean;
|
|
14
|
+
currentTime: number;
|
|
15
|
+
duration: number;
|
|
16
|
+
liveEdge: number;
|
|
17
|
+
seekableStart: number;
|
|
18
|
+
/** Unix timestamp (ms) at stream time 0 - for wall-clock display */
|
|
19
|
+
unixoffset?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Pure Functions
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format seconds as MM:SS or HH:MM:SS.
|
|
28
|
+
*
|
|
29
|
+
* @param seconds - Time in seconds
|
|
30
|
+
* @returns Formatted time string, or "LIVE" for invalid input
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* formatTime(65) // "01:05"
|
|
34
|
+
* formatTime(3665) // "1:01:05"
|
|
35
|
+
* formatTime(-1) // "LIVE"
|
|
36
|
+
* formatTime(NaN) // "LIVE"
|
|
37
|
+
*/
|
|
38
|
+
export function formatTime(seconds: number): string {
|
|
39
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
40
|
+
return 'LIVE';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const total = Math.floor(seconds);
|
|
44
|
+
const hours = Math.floor(total / 3600);
|
|
45
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
46
|
+
const secs = total % 60;
|
|
47
|
+
|
|
48
|
+
if (hours > 0) {
|
|
49
|
+
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a Date as wall-clock time (HH:MM:SS).
|
|
57
|
+
*
|
|
58
|
+
* @param date - Date object
|
|
59
|
+
* @returns Formatted time string in HH:MM:SS format
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* formatClockTime(new Date('2024-01-15T14:30:45')) // "14:30:45"
|
|
63
|
+
*/
|
|
64
|
+
export function formatClockTime(date: Date): string {
|
|
65
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
66
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
67
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
68
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format time display for player controls.
|
|
73
|
+
*
|
|
74
|
+
* For live streams:
|
|
75
|
+
* - With unixoffset: Shows actual wall-clock time (HH:MM:SS)
|
|
76
|
+
* - With seekable window: Shows time behind live (-MM:SS) or "LIVE"
|
|
77
|
+
* - Fallback: Shows elapsed time
|
|
78
|
+
*
|
|
79
|
+
* For VOD:
|
|
80
|
+
* - Shows "current / duration" (MM:SS / MM:SS)
|
|
81
|
+
*
|
|
82
|
+
* @param params - Display parameters
|
|
83
|
+
* @returns Formatted time display string
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Live with unixoffset
|
|
87
|
+
* formatTimeDisplay({ isLive: true, currentTime: 60, unixoffset: 1705330245000, ... })
|
|
88
|
+
* // "14:30:45"
|
|
89
|
+
*
|
|
90
|
+
* // Live behind
|
|
91
|
+
* formatTimeDisplay({ isLive: true, currentTime: 50, liveEdge: 60, ... })
|
|
92
|
+
* // "-00:10"
|
|
93
|
+
*
|
|
94
|
+
* // VOD
|
|
95
|
+
* formatTimeDisplay({ isLive: false, currentTime: 65, duration: 300, ... })
|
|
96
|
+
* // "01:05 / 05:00"
|
|
97
|
+
*/
|
|
98
|
+
export function formatTimeDisplay(params: TimeDisplayParams): string {
|
|
99
|
+
const { isLive, currentTime, duration, liveEdge, seekableStart, unixoffset } = params;
|
|
100
|
+
|
|
101
|
+
if (isLive) {
|
|
102
|
+
// For live: show actual wall-clock time using unixoffset
|
|
103
|
+
if (unixoffset && unixoffset > 0) {
|
|
104
|
+
// unixoffset is Unix timestamp in ms at timestamp 0 of the stream
|
|
105
|
+
// currentTime is playback position in seconds
|
|
106
|
+
const actualTimeMs = unixoffset + (currentTime * 1000);
|
|
107
|
+
const actualDate = new Date(actualTimeMs);
|
|
108
|
+
return formatClockTime(actualDate);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fallback: show relative time if no unixoffset
|
|
112
|
+
const seekableWindow = liveEdge - seekableStart;
|
|
113
|
+
if (seekableWindow > 0) {
|
|
114
|
+
const behindSeconds = liveEdge - currentTime;
|
|
115
|
+
if (behindSeconds < 1) {
|
|
116
|
+
return 'LIVE';
|
|
117
|
+
}
|
|
118
|
+
return `-${formatTime(Math.abs(behindSeconds))}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No DVR window: show LIVE instead of a misleading timestamp
|
|
122
|
+
return 'LIVE';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// VOD: show current / total
|
|
126
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
127
|
+
return `${formatTime(currentTime)} / ${formatTime(duration)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return formatTime(currentTime);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format time for seek bar tooltip.
|
|
135
|
+
* For live streams, can show time relative to live edge.
|
|
136
|
+
*
|
|
137
|
+
* @param time - Time position in seconds
|
|
138
|
+
* @param isLive - Whether stream is live
|
|
139
|
+
* @param liveEdge - Live edge position (for relative display)
|
|
140
|
+
* @returns Formatted tooltip time
|
|
141
|
+
*/
|
|
142
|
+
export function formatTooltipTime(
|
|
143
|
+
time: number,
|
|
144
|
+
isLive: boolean,
|
|
145
|
+
liveEdge?: number
|
|
146
|
+
): string {
|
|
147
|
+
if (isLive && liveEdge !== undefined && Number.isFinite(liveEdge)) {
|
|
148
|
+
const behindSeconds = liveEdge - time;
|
|
149
|
+
if (behindSeconds < 1) {
|
|
150
|
+
return 'LIVE';
|
|
151
|
+
}
|
|
152
|
+
return `-${formatTime(Math.abs(behindSeconds))}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return formatTime(time);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format duration for display (e.g., in stats panel).
|
|
160
|
+
* Handles edge cases like infinite duration for live streams.
|
|
161
|
+
*
|
|
162
|
+
* @param duration - Duration in seconds
|
|
163
|
+
* @param isLive - Whether content is live
|
|
164
|
+
* @returns Formatted duration string
|
|
165
|
+
*/
|
|
166
|
+
export function formatDuration(duration: number, isLive?: boolean): string {
|
|
167
|
+
if (isLive || !Number.isFinite(duration)) {
|
|
168
|
+
return 'LIVE';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return formatTime(duration);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse time string (HH:MM:SS or MM:SS) to seconds.
|
|
176
|
+
*
|
|
177
|
+
* @param timeStr - Time string to parse
|
|
178
|
+
* @returns Time in seconds, or NaN if invalid
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* parseTime("01:30") // 90
|
|
182
|
+
* parseTime("1:30:45") // 5445
|
|
183
|
+
* parseTime("invalid") // NaN
|
|
184
|
+
*/
|
|
185
|
+
export function parseTime(timeStr: string): number {
|
|
186
|
+
const parts = timeStr.split(':').map(Number);
|
|
187
|
+
|
|
188
|
+
if (parts.some(isNaN)) {
|
|
189
|
+
return NaN;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (parts.length === 2) {
|
|
193
|
+
// MM:SS
|
|
194
|
+
const [minutes, seconds] = parts;
|
|
195
|
+
return minutes * 60 + seconds;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (parts.length === 3) {
|
|
199
|
+
// HH:MM:SS
|
|
200
|
+
const [hours, minutes, seconds] = parts;
|
|
201
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return NaN;
|
|
205
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimerManager - Centralized timer management for memory leak prevention
|
|
3
|
+
*
|
|
4
|
+
* Tracks all setTimeout/setInterval calls and provides bulk cleanup.
|
|
5
|
+
* Based on MistMetaPlayer's MistVideo.timers pattern.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* const timers = new TimerManager();
|
|
10
|
+
*
|
|
11
|
+
* // Start a timeout
|
|
12
|
+
* const id = timers.start(() => console.log('fired'), 1000);
|
|
13
|
+
*
|
|
14
|
+
* // Start an interval
|
|
15
|
+
* const intervalId = timers.startInterval(() => console.log('tick'), 500);
|
|
16
|
+
*
|
|
17
|
+
* // Stop a specific timer
|
|
18
|
+
* timers.stop(id);
|
|
19
|
+
*
|
|
20
|
+
* // Stop all timers (on cleanup/destroy)
|
|
21
|
+
* timers.stopAll();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface TimerEntry {
|
|
26
|
+
/** Timer ID from setTimeout/setInterval */
|
|
27
|
+
id: ReturnType<typeof setTimeout>;
|
|
28
|
+
/** Expected end time (for timeouts) */
|
|
29
|
+
endTime: number;
|
|
30
|
+
/** Whether this is an interval */
|
|
31
|
+
isInterval: boolean;
|
|
32
|
+
/** Optional label for debugging */
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class TimerManager {
|
|
37
|
+
private timers: Map<number, TimerEntry> = new Map();
|
|
38
|
+
private nextId = 1;
|
|
39
|
+
private debug: boolean;
|
|
40
|
+
|
|
41
|
+
constructor(options?: { debug?: boolean }) {
|
|
42
|
+
this.debug = options?.debug ?? false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Start a timeout
|
|
47
|
+
* @param callback Function to call after delay
|
|
48
|
+
* @param delay Delay in milliseconds
|
|
49
|
+
* @param label Optional label for debugging
|
|
50
|
+
* @returns Timer ID (internal, not the native timeout ID)
|
|
51
|
+
*/
|
|
52
|
+
start(callback: () => void, delay: number, label?: string): number {
|
|
53
|
+
const internalId = this.nextId++;
|
|
54
|
+
const endTime = Date.now() + delay;
|
|
55
|
+
|
|
56
|
+
const nativeId = setTimeout(() => {
|
|
57
|
+
this.timers.delete(internalId);
|
|
58
|
+
try {
|
|
59
|
+
callback();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error('[TimerManager] Callback error:', e);
|
|
62
|
+
}
|
|
63
|
+
}, delay);
|
|
64
|
+
|
|
65
|
+
this.timers.set(internalId, {
|
|
66
|
+
id: nativeId,
|
|
67
|
+
endTime,
|
|
68
|
+
isInterval: false,
|
|
69
|
+
label,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (this.debug) {
|
|
73
|
+
console.debug(`[TimerManager] Started timeout ${internalId}${label ? ` (${label})` : ''} for ${delay}ms`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return internalId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start an interval
|
|
81
|
+
* @param callback Function to call repeatedly
|
|
82
|
+
* @param interval Interval in milliseconds
|
|
83
|
+
* @param label Optional label for debugging
|
|
84
|
+
* @returns Timer ID (internal, not the native interval ID)
|
|
85
|
+
*/
|
|
86
|
+
startInterval(callback: () => void, interval: number, label?: string): number {
|
|
87
|
+
const internalId = this.nextId++;
|
|
88
|
+
|
|
89
|
+
const nativeId = setInterval(() => {
|
|
90
|
+
try {
|
|
91
|
+
callback();
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error('[TimerManager] Interval callback error:', e);
|
|
94
|
+
}
|
|
95
|
+
}, interval);
|
|
96
|
+
|
|
97
|
+
this.timers.set(internalId, {
|
|
98
|
+
id: nativeId,
|
|
99
|
+
endTime: Infinity, // Intervals don't have an end time
|
|
100
|
+
isInterval: true,
|
|
101
|
+
label,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (this.debug) {
|
|
105
|
+
console.debug(`[TimerManager] Started interval ${internalId}${label ? ` (${label})` : ''} every ${interval}ms`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return internalId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stop a specific timer
|
|
113
|
+
* @param internalId The timer ID returned by start() or startInterval()
|
|
114
|
+
*/
|
|
115
|
+
stop(internalId: number): boolean {
|
|
116
|
+
const entry = this.timers.get(internalId);
|
|
117
|
+
if (!entry) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (entry.isInterval) {
|
|
122
|
+
clearInterval(entry.id);
|
|
123
|
+
} else {
|
|
124
|
+
clearTimeout(entry.id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.timers.delete(internalId);
|
|
128
|
+
|
|
129
|
+
if (this.debug) {
|
|
130
|
+
console.debug(`[TimerManager] Stopped ${entry.isInterval ? 'interval' : 'timeout'} ${internalId}${entry.label ? ` (${entry.label})` : ''}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stop all active timers
|
|
138
|
+
* Call this on component unmount/destroy to prevent memory leaks
|
|
139
|
+
*/
|
|
140
|
+
stopAll(): void {
|
|
141
|
+
const count = this.timers.size;
|
|
142
|
+
|
|
143
|
+
for (const [internalId, entry] of this.timers) {
|
|
144
|
+
if (entry.isInterval) {
|
|
145
|
+
clearInterval(entry.id);
|
|
146
|
+
} else {
|
|
147
|
+
clearTimeout(entry.id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.timers.clear();
|
|
152
|
+
|
|
153
|
+
if (this.debug && count > 0) {
|
|
154
|
+
console.debug(`[TimerManager] Stopped all ${count} timers`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get count of active timers
|
|
160
|
+
*/
|
|
161
|
+
get activeCount(): number {
|
|
162
|
+
return this.timers.size;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a timer is active
|
|
167
|
+
*/
|
|
168
|
+
isActive(internalId: number): boolean {
|
|
169
|
+
return this.timers.has(internalId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get remaining time for a timeout (0 for intervals or expired)
|
|
174
|
+
*/
|
|
175
|
+
getRemainingTime(internalId: number): number {
|
|
176
|
+
const entry = this.timers.get(internalId);
|
|
177
|
+
if (!entry || entry.isInterval) {
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
return Math.max(0, entry.endTime - Date.now());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get debug info about all active timers
|
|
185
|
+
*/
|
|
186
|
+
getDebugInfo(): Array<{ id: number; type: 'timeout' | 'interval'; label?: string; remainingMs?: number }> {
|
|
187
|
+
const info: Array<{ id: number; type: 'timeout' | 'interval'; label?: string; remainingMs?: number }> = [];
|
|
188
|
+
|
|
189
|
+
for (const [internalId, entry] of this.timers) {
|
|
190
|
+
info.push({
|
|
191
|
+
id: internalId,
|
|
192
|
+
type: entry.isInterval ? 'interval' : 'timeout',
|
|
193
|
+
label: entry.label,
|
|
194
|
+
remainingMs: entry.isInterval ? undefined : Math.max(0, entry.endTime - Date.now()),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return info;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Cleanup - alias for stopAll()
|
|
203
|
+
*/
|
|
204
|
+
destroy(): void {
|
|
205
|
+
this.stopAll();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default TimerManager;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UrlUtils - URL manipulation utilities
|
|
3
|
+
*
|
|
4
|
+
* Based on MistMetaPlayer's urlappend functionality.
|
|
5
|
+
* Provides helpers for appending query parameters to URLs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Append query parameters to a URL
|
|
10
|
+
* Handles URLs that already have query parameters
|
|
11
|
+
*
|
|
12
|
+
* @param url - Base URL
|
|
13
|
+
* @param params - Parameters to append (string or object)
|
|
14
|
+
* @returns URL with appended parameters
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* appendUrlParams('https://example.com/video.m3u8', 'token=abc&session=123')
|
|
19
|
+
* // => 'https://example.com/video.m3u8?token=abc&session=123'
|
|
20
|
+
*
|
|
21
|
+
* appendUrlParams('https://example.com/video.m3u8?existing=param', 'token=abc')
|
|
22
|
+
* // => 'https://example.com/video.m3u8?existing=param&token=abc'
|
|
23
|
+
*
|
|
24
|
+
* appendUrlParams('https://example.com/video.m3u8', { token: 'abc', session: '123' })
|
|
25
|
+
* // => 'https://example.com/video.m3u8?token=abc&session=123'
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function appendUrlParams(
|
|
29
|
+
url: string,
|
|
30
|
+
params: string | Record<string, string | number | boolean | undefined | null>
|
|
31
|
+
): string {
|
|
32
|
+
if (!params) {
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Convert object to query string
|
|
37
|
+
let queryString: string;
|
|
38
|
+
if (typeof params === 'object') {
|
|
39
|
+
const entries = Object.entries(params)
|
|
40
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
41
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
42
|
+
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
queryString = entries.join('&');
|
|
47
|
+
} else {
|
|
48
|
+
queryString = params;
|
|
49
|
+
// Strip leading ? or & if present
|
|
50
|
+
if (queryString.startsWith('?') || queryString.startsWith('&')) {
|
|
51
|
+
queryString = queryString.slice(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!queryString) {
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine separator (? or &)
|
|
60
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
61
|
+
return `${url}${separator}${queryString}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse query parameters from a URL
|
|
66
|
+
*
|
|
67
|
+
* @param url - URL to parse
|
|
68
|
+
* @returns Object with query parameters
|
|
69
|
+
*/
|
|
70
|
+
export function parseUrlParams(url: string): Record<string, string> {
|
|
71
|
+
const params: Record<string, string> = {};
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const urlObj = new URL(url);
|
|
75
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
76
|
+
params[key] = value;
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
// If URL parsing fails, try manual parsing
|
|
80
|
+
const queryIndex = url.indexOf('?');
|
|
81
|
+
if (queryIndex === -1) {
|
|
82
|
+
return params;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const queryString = url.slice(queryIndex + 1);
|
|
86
|
+
const pairs = queryString.split('&');
|
|
87
|
+
for (const pair of pairs) {
|
|
88
|
+
const [key, value] = pair.split('=');
|
|
89
|
+
if (key) {
|
|
90
|
+
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return params;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove query parameters from a URL
|
|
100
|
+
*
|
|
101
|
+
* @param url - URL to strip
|
|
102
|
+
* @returns URL without query parameters
|
|
103
|
+
*/
|
|
104
|
+
export function stripUrlParams(url: string): string {
|
|
105
|
+
const queryIndex = url.indexOf('?');
|
|
106
|
+
return queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a URL with query parameters
|
|
111
|
+
*
|
|
112
|
+
* @param baseUrl - Base URL
|
|
113
|
+
* @param params - Query parameters
|
|
114
|
+
* @returns Complete URL
|
|
115
|
+
*/
|
|
116
|
+
export function buildUrl(baseUrl: string, params: Record<string, string | number | boolean | undefined | null>): string {
|
|
117
|
+
return appendUrlParams(stripUrlParams(baseUrl), params);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if URL uses secure protocol (https/wss)
|
|
122
|
+
*/
|
|
123
|
+
export function isSecureUrl(url: string): boolean {
|
|
124
|
+
return url.startsWith('https://') || url.startsWith('wss://');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert HTTP URL to WebSocket URL
|
|
129
|
+
* http:// -> ws://
|
|
130
|
+
* https:// -> wss://
|
|
131
|
+
*/
|
|
132
|
+
export function httpToWs(url: string): string {
|
|
133
|
+
return url.replace(/^http/, 'ws');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert WebSocket URL to HTTP URL
|
|
138
|
+
* ws:// -> http://
|
|
139
|
+
* wss:// -> https://
|
|
140
|
+
*/
|
|
141
|
+
export function wsToHttp(url: string): string {
|
|
142
|
+
return url.replace(/^ws/, 'http');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ensure URL uses the same protocol as the current page
|
|
147
|
+
* Useful for avoiding mixed content issues
|
|
148
|
+
*/
|
|
149
|
+
export function matchPageProtocol(url: string): string {
|
|
150
|
+
if (typeof window === 'undefined') {
|
|
151
|
+
return url;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const pageIsSecure = window.location.protocol === 'https:';
|
|
155
|
+
const urlIsSecure = isSecureUrl(url);
|
|
156
|
+
|
|
157
|
+
if (pageIsSecure && !urlIsSecure) {
|
|
158
|
+
// Upgrade to secure
|
|
159
|
+
return url.replace(/^http:/, 'https:').replace(/^ws:/, 'wss:');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!pageIsSecure && urlIsSecure) {
|
|
163
|
+
// Downgrade to insecure (not recommended, but avoids issues)
|
|
164
|
+
return url.replace(/^https:/, 'http:').replace(/^wss:/, 'ws:');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return url;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default {
|
|
171
|
+
appendUrlParams,
|
|
172
|
+
parseUrlParams,
|
|
173
|
+
stripUrlParams,
|
|
174
|
+
buildUrl,
|
|
175
|
+
isSecureUrl,
|
|
176
|
+
httpToWs,
|
|
177
|
+
wsToHttp,
|
|
178
|
+
matchPageProtocol,
|
|
179
|
+
};
|