@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.
Files changed (120) hide show
  1. package/dist/cjs/index.js +19493 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +19398 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/player.css +2140 -0
  6. package/dist/types/core/ABRController.d.ts +164 -0
  7. package/dist/types/core/CodecUtils.d.ts +54 -0
  8. package/dist/types/core/Disposable.d.ts +61 -0
  9. package/dist/types/core/EventEmitter.d.ts +73 -0
  10. package/dist/types/core/GatewayClient.d.ts +144 -0
  11. package/dist/types/core/InteractionController.d.ts +121 -0
  12. package/dist/types/core/LiveDurationProxy.d.ts +102 -0
  13. package/dist/types/core/MetaTrackManager.d.ts +220 -0
  14. package/dist/types/core/MistReporter.d.ts +163 -0
  15. package/dist/types/core/MistSignaling.d.ts +148 -0
  16. package/dist/types/core/PlayerController.d.ts +665 -0
  17. package/dist/types/core/PlayerInterface.d.ts +230 -0
  18. package/dist/types/core/PlayerManager.d.ts +182 -0
  19. package/dist/types/core/PlayerRegistry.d.ts +27 -0
  20. package/dist/types/core/QualityMonitor.d.ts +184 -0
  21. package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
  22. package/dist/types/core/SeekingUtils.d.ts +142 -0
  23. package/dist/types/core/StreamStateClient.d.ts +108 -0
  24. package/dist/types/core/SubtitleManager.d.ts +111 -0
  25. package/dist/types/core/TelemetryReporter.d.ts +79 -0
  26. package/dist/types/core/TimeFormat.d.ts +97 -0
  27. package/dist/types/core/TimerManager.d.ts +83 -0
  28. package/dist/types/core/UrlUtils.d.ts +81 -0
  29. package/dist/types/core/detector.d.ts +149 -0
  30. package/dist/types/core/index.d.ts +49 -0
  31. package/dist/types/core/scorer.d.ts +167 -0
  32. package/dist/types/core/selector.d.ts +9 -0
  33. package/dist/types/index.d.ts +45 -0
  34. package/dist/types/lib/utils.d.ts +2 -0
  35. package/dist/types/players/DashJsPlayer.d.ts +102 -0
  36. package/dist/types/players/HlsJsPlayer.d.ts +70 -0
  37. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
  38. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
  39. package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
  40. package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
  41. package/dist/types/players/MistPlayer.d.ts +25 -0
  42. package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
  43. package/dist/types/players/NativePlayer.d.ts +143 -0
  44. package/dist/types/players/VideoJsPlayer.d.ts +59 -0
  45. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
  46. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
  47. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
  48. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
  49. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
  50. package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
  51. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
  52. package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
  53. package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
  54. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
  55. package/dist/types/players/index.d.ts +14 -0
  56. package/dist/types/styles/index.d.ts +11 -0
  57. package/dist/types/types.d.ts +363 -0
  58. package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
  59. package/dist/types/vanilla/index.d.ts +19 -0
  60. package/dist/workers/decoder.worker.js +989 -0
  61. package/dist/workers/decoder.worker.js.map +1 -0
  62. package/package.json +80 -0
  63. package/src/core/ABRController.ts +550 -0
  64. package/src/core/CodecUtils.ts +257 -0
  65. package/src/core/Disposable.ts +120 -0
  66. package/src/core/EventEmitter.ts +113 -0
  67. package/src/core/GatewayClient.ts +439 -0
  68. package/src/core/InteractionController.ts +712 -0
  69. package/src/core/LiveDurationProxy.ts +270 -0
  70. package/src/core/MetaTrackManager.ts +753 -0
  71. package/src/core/MistReporter.ts +543 -0
  72. package/src/core/MistSignaling.ts +346 -0
  73. package/src/core/PlayerController.ts +2829 -0
  74. package/src/core/PlayerInterface.ts +432 -0
  75. package/src/core/PlayerManager.ts +900 -0
  76. package/src/core/PlayerRegistry.ts +149 -0
  77. package/src/core/QualityMonitor.ts +597 -0
  78. package/src/core/ScreenWakeLockManager.ts +163 -0
  79. package/src/core/SeekingUtils.ts +364 -0
  80. package/src/core/StreamStateClient.ts +457 -0
  81. package/src/core/SubtitleManager.ts +297 -0
  82. package/src/core/TelemetryReporter.ts +308 -0
  83. package/src/core/TimeFormat.ts +205 -0
  84. package/src/core/TimerManager.ts +209 -0
  85. package/src/core/UrlUtils.ts +179 -0
  86. package/src/core/detector.ts +382 -0
  87. package/src/core/index.ts +140 -0
  88. package/src/core/scorer.ts +553 -0
  89. package/src/core/selector.ts +16 -0
  90. package/src/global.d.ts +11 -0
  91. package/src/index.ts +75 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/players/DashJsPlayer.ts +642 -0
  94. package/src/players/HlsJsPlayer.ts +483 -0
  95. package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
  96. package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
  97. package/src/players/MewsWsPlayer/index.ts +1065 -0
  98. package/src/players/MewsWsPlayer/types.ts +106 -0
  99. package/src/players/MistPlayer.ts +188 -0
  100. package/src/players/MistWebRTCPlayer/index.ts +703 -0
  101. package/src/players/NativePlayer.ts +820 -0
  102. package/src/players/VideoJsPlayer.ts +643 -0
  103. package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
  104. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
  105. package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
  106. package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
  107. package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
  108. package/src/players/WebCodecsPlayer/index.ts +1650 -0
  109. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
  110. package/src/players/WebCodecsPlayer/types.ts +542 -0
  111. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
  112. package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
  113. package/src/players/index.ts +22 -0
  114. package/src/styles/animations.css +21 -0
  115. package/src/styles/index.ts +52 -0
  116. package/src/styles/player.css +2126 -0
  117. package/src/styles/tailwind.css +1015 -0
  118. package/src/types.ts +421 -0
  119. package/src/vanilla/FrameWorksPlayer.ts +367 -0
  120. 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
+ };