@livepeer-frameworks/player-react 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +7 -9
  2. package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js +359 -0
  3. package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  4. package/dist/cjs/assets/logomark.svg.js +8 -0
  5. package/dist/cjs/assets/logomark.svg.js.map +1 -0
  6. package/dist/cjs/components/DevModePanel.js +826 -0
  7. package/dist/cjs/components/DevModePanel.js.map +1 -0
  8. package/dist/cjs/components/DvdLogo.js +200 -0
  9. package/dist/cjs/components/DvdLogo.js.map +1 -0
  10. package/dist/cjs/components/Icons.js +439 -0
  11. package/dist/cjs/components/Icons.js.map +1 -0
  12. package/dist/cjs/components/IdleScreen.js +587 -0
  13. package/dist/cjs/components/IdleScreen.js.map +1 -0
  14. package/dist/cjs/components/LoadingScreen.js +523 -0
  15. package/dist/cjs/components/LoadingScreen.js.map +1 -0
  16. package/dist/cjs/components/Player.js +420 -0
  17. package/dist/cjs/components/Player.js.map +1 -0
  18. package/dist/cjs/components/PlayerControls.js +798 -0
  19. package/dist/cjs/components/PlayerControls.js.map +1 -0
  20. package/dist/cjs/components/PlayerErrorBoundary.js +80 -0
  21. package/dist/cjs/components/PlayerErrorBoundary.js.map +1 -0
  22. package/dist/cjs/components/SeekBar.js +253 -0
  23. package/dist/cjs/components/SeekBar.js.map +1 -0
  24. package/dist/cjs/components/SkipIndicator.js +92 -0
  25. package/dist/cjs/components/SkipIndicator.js.map +1 -0
  26. package/dist/cjs/components/SpeedIndicator.js +43 -0
  27. package/dist/cjs/components/SpeedIndicator.js.map +1 -0
  28. package/dist/cjs/components/StatsPanel.js +202 -0
  29. package/dist/cjs/components/StatsPanel.js.map +1 -0
  30. package/dist/cjs/components/StreamStateOverlay.js +229 -0
  31. package/dist/cjs/components/StreamStateOverlay.js.map +1 -0
  32. package/dist/cjs/components/ThumbnailOverlay.js +86 -0
  33. package/dist/cjs/components/ThumbnailOverlay.js.map +1 -0
  34. package/dist/cjs/components/TitleOverlay.js +32 -0
  35. package/dist/cjs/components/TitleOverlay.js.map +1 -0
  36. package/dist/cjs/context/PlayerContext.js +46 -0
  37. package/dist/cjs/context/PlayerContext.js.map +1 -0
  38. package/dist/cjs/hooks/useMetaTrack.js +165 -0
  39. package/dist/cjs/hooks/useMetaTrack.js.map +1 -0
  40. package/dist/cjs/hooks/usePlaybackQuality.js +131 -0
  41. package/dist/cjs/hooks/usePlaybackQuality.js.map +1 -0
  42. package/dist/cjs/hooks/usePlayerController.js +518 -0
  43. package/dist/cjs/hooks/usePlayerController.js.map +1 -0
  44. package/dist/cjs/hooks/usePlayerSelection.js +90 -0
  45. package/dist/cjs/hooks/usePlayerSelection.js.map +1 -0
  46. package/dist/cjs/hooks/useStreamState.js +360 -0
  47. package/dist/cjs/hooks/useStreamState.js.map +1 -0
  48. package/dist/cjs/hooks/useTelemetry.js +120 -0
  49. package/dist/cjs/hooks/useTelemetry.js.map +1 -0
  50. package/dist/cjs/hooks/useViewerEndpoints.js +222 -0
  51. package/dist/cjs/hooks/useViewerEndpoints.js.map +1 -0
  52. package/dist/cjs/index.js +97 -1
  53. package/dist/cjs/index.js.map +1 -1
  54. package/dist/cjs/ui/badge.js +34 -0
  55. package/dist/cjs/ui/badge.js.map +1 -0
  56. package/dist/cjs/ui/button.js +74 -0
  57. package/dist/cjs/ui/button.js.map +1 -0
  58. package/dist/cjs/ui/context-menu.js +163 -0
  59. package/dist/cjs/ui/context-menu.js.map +1 -0
  60. package/dist/cjs/ui/slider.js +60 -0
  61. package/dist/cjs/ui/slider.js.map +1 -0
  62. package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +329 -0
  63. package/dist/esm/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  64. package/dist/esm/assets/logomark.svg.js +4 -0
  65. package/dist/esm/assets/logomark.svg.js.map +1 -0
  66. package/dist/esm/components/DevModePanel.js +822 -0
  67. package/dist/esm/components/DevModePanel.js.map +1 -0
  68. package/dist/esm/components/DvdLogo.js +196 -0
  69. package/dist/esm/components/DvdLogo.js.map +1 -0
  70. package/dist/esm/components/Icons.js +421 -0
  71. package/dist/esm/components/Icons.js.map +1 -0
  72. package/dist/esm/components/IdleScreen.js +582 -0
  73. package/dist/esm/components/IdleScreen.js.map +1 -0
  74. package/dist/esm/components/LoadingScreen.js +519 -0
  75. package/dist/esm/components/LoadingScreen.js.map +1 -0
  76. package/dist/esm/components/Player.js +416 -0
  77. package/dist/esm/components/Player.js.map +1 -0
  78. package/dist/esm/components/PlayerControls.js +794 -0
  79. package/dist/esm/components/PlayerControls.js.map +1 -0
  80. package/dist/esm/components/PlayerErrorBoundary.js +76 -0
  81. package/dist/esm/components/PlayerErrorBoundary.js.map +1 -0
  82. package/dist/esm/components/SeekBar.js +249 -0
  83. package/dist/esm/components/SeekBar.js.map +1 -0
  84. package/dist/esm/components/SkipIndicator.js +88 -0
  85. package/dist/esm/components/SkipIndicator.js.map +1 -0
  86. package/dist/esm/components/SpeedIndicator.js +39 -0
  87. package/dist/esm/components/SpeedIndicator.js.map +1 -0
  88. package/dist/esm/components/StatsPanel.js +198 -0
  89. package/dist/esm/components/StatsPanel.js.map +1 -0
  90. package/dist/esm/components/StreamStateOverlay.js +224 -0
  91. package/dist/esm/components/StreamStateOverlay.js.map +1 -0
  92. package/dist/esm/components/ThumbnailOverlay.js +82 -0
  93. package/dist/esm/components/ThumbnailOverlay.js.map +1 -0
  94. package/dist/esm/components/TitleOverlay.js +28 -0
  95. package/dist/esm/components/TitleOverlay.js.map +1 -0
  96. package/dist/esm/context/PlayerContext.js +41 -0
  97. package/dist/esm/context/PlayerContext.js.map +1 -0
  98. package/dist/esm/hooks/useMetaTrack.js +163 -0
  99. package/dist/esm/hooks/useMetaTrack.js.map +1 -0
  100. package/dist/esm/hooks/usePlaybackQuality.js +129 -0
  101. package/dist/esm/hooks/usePlaybackQuality.js.map +1 -0
  102. package/dist/esm/hooks/usePlayerController.js +516 -0
  103. package/dist/esm/hooks/usePlayerController.js.map +1 -0
  104. package/dist/esm/hooks/usePlayerSelection.js +88 -0
  105. package/dist/esm/hooks/usePlayerSelection.js.map +1 -0
  106. package/dist/esm/hooks/useStreamState.js +358 -0
  107. package/dist/esm/hooks/useStreamState.js.map +1 -0
  108. package/dist/esm/hooks/useTelemetry.js +118 -0
  109. package/dist/esm/hooks/useTelemetry.js.map +1 -0
  110. package/dist/esm/hooks/useViewerEndpoints.js +220 -0
  111. package/dist/esm/hooks/useViewerEndpoints.js.map +1 -0
  112. package/dist/esm/index.js +23 -1
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/ui/badge.js +31 -0
  115. package/dist/esm/ui/badge.js.map +1 -0
  116. package/dist/esm/ui/button.js +52 -0
  117. package/dist/esm/ui/button.js.map +1 -0
  118. package/dist/esm/ui/context-menu.js +132 -0
  119. package/dist/esm/ui/context-menu.js.map +1 -0
  120. package/dist/esm/ui/slider.js +38 -0
  121. package/dist/esm/ui/slider.js.map +1 -0
  122. package/dist/types/components/DvdLogo.d.ts +1 -1
  123. package/dist/types/components/Icons.d.ts +1 -1
  124. package/dist/types/components/Player.d.ts +1 -1
  125. package/dist/types/components/PlayerErrorBoundary.d.ts +2 -1
  126. package/dist/types/components/StreamStateOverlay.d.ts +2 -2
  127. package/dist/types/components/SubtitleRenderer.d.ts +2 -2
  128. package/dist/types/context/PlayerContext.d.ts +2 -2
  129. package/dist/types/context/index.d.ts +2 -2
  130. package/dist/types/hooks/useMetaTrack.d.ts +3 -3
  131. package/dist/types/hooks/usePlaybackQuality.d.ts +2 -2
  132. package/dist/types/hooks/usePlayerController.d.ts +26 -3
  133. package/dist/types/hooks/usePlayerSelection.d.ts +1 -1
  134. package/dist/types/hooks/useStreamState.d.ts +1 -1
  135. package/dist/types/hooks/useTelemetry.d.ts +1 -1
  136. package/dist/types/hooks/useViewerEndpoints.d.ts +3 -3
  137. package/dist/types/index.d.ts +28 -28
  138. package/dist/types/types.d.ts +3 -3
  139. package/dist/types/ui/select.d.ts +1 -1
  140. package/package.json +22 -14
  141. package/src/components/DevModePanel.tsx +244 -143
  142. package/src/components/DvdLogo.tsx +1 -1
  143. package/src/components/Icons.tsx +105 -25
  144. package/src/components/IdleScreen.tsx +262 -128
  145. package/src/components/LoadingScreen.tsx +169 -151
  146. package/src/components/LogoOverlay.tsx +3 -6
  147. package/src/components/Player.tsx +126 -59
  148. package/src/components/PlayerControls.tsx +384 -272
  149. package/src/components/PlayerErrorBoundary.tsx +7 -13
  150. package/src/components/SeekBar.tsx +96 -88
  151. package/src/components/SkipIndicator.tsx +2 -12
  152. package/src/components/SpeedIndicator.tsx +2 -11
  153. package/src/components/StatsPanel.tsx +31 -22
  154. package/src/components/StreamStateOverlay.tsx +105 -49
  155. package/src/components/SubtitleRenderer.tsx +29 -29
  156. package/src/components/ThumbnailOverlay.tsx +5 -6
  157. package/src/components/TitleOverlay.tsx +2 -8
  158. package/src/context/PlayerContext.tsx +4 -8
  159. package/src/context/index.ts +3 -3
  160. package/src/hooks/useMetaTrack.ts +27 -27
  161. package/src/hooks/usePlaybackQuality.ts +3 -3
  162. package/src/hooks/usePlayerController.ts +246 -138
  163. package/src/hooks/usePlayerSelection.ts +6 -6
  164. package/src/hooks/useStreamState.ts +51 -56
  165. package/src/hooks/useTelemetry.ts +18 -3
  166. package/src/hooks/useViewerEndpoints.ts +34 -23
  167. package/src/index.tsx +36 -28
  168. package/src/types.ts +8 -8
  169. package/src/ui/badge.tsx +6 -5
  170. package/src/ui/button.tsx +9 -8
  171. package/src/ui/context-menu.tsx +42 -61
  172. package/src/ui/select.tsx +13 -7
  173. package/src/ui/slider.tsx +18 -29
  174. package/dist/types/components/players/DashJsPlayer.d.ts +0 -18
  175. package/dist/types/components/players/HlsJsPlayer.d.ts +0 -18
  176. package/dist/types/components/players/MewsWsPlayer/index.d.ts +0 -18
  177. package/dist/types/components/players/MistPlayer.d.ts +0 -20
  178. package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +0 -20
  179. package/dist/types/components/players/NativePlayer.d.ts +0 -19
  180. package/dist/types/components/players/VideoJsPlayer.d.ts +0 -18
  181. package/src/components/players/DashJsPlayer.tsx +0 -56
  182. package/src/components/players/HlsJsPlayer.tsx +0 -56
  183. package/src/components/players/MewsWsPlayer/index.tsx +0 -56
  184. package/src/components/players/MistPlayer.tsx +0 -60
  185. package/src/components/players/MistWebRTCPlayer/index.tsx +0 -59
  186. package/src/components/players/NativePlayer.tsx +0 -58
  187. package/src/components/players/VideoJsPlayer.tsx +0 -56
@@ -1,5 +1,6 @@
1
- import React, { Component, ErrorInfo, ReactNode } from 'react';
2
- import { Button } from '../ui/button';
1
+ import type { ErrorInfo, ReactNode } from "react";
2
+ import React, { Component } from "react";
3
+ import { Button } from "../ui/button";
3
4
 
4
5
  interface Props {
5
6
  children: ReactNode;
@@ -28,7 +29,7 @@ class PlayerErrorBoundary extends Component<Props, State> {
28
29
  }
29
30
 
30
31
  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
31
- console.error('[PlayerErrorBoundary] Caught error:', error, errorInfo);
32
+ console.error("[PlayerErrorBoundary] Caught error:", error, errorInfo);
32
33
  this.props.onError?.(error, errorInfo);
33
34
  }
34
35
 
@@ -45,18 +46,11 @@ class PlayerErrorBoundary extends Component<Props, State> {
45
46
 
46
47
  return (
47
48
  <div className="fw-player-error flex min-h-[280px] flex-col items-center justify-center gap-4 rounded-xl bg-slate-950 p-6 text-center text-white">
48
- <div className="text-lg font-semibold text-red-400">
49
- Playback Error
50
- </div>
49
+ <div className="text-lg font-semibold text-red-400">Playback Error</div>
51
50
  <p className="max-w-sm text-sm text-slate-400">
52
- {this.state.error?.message || 'An unexpected error occurred while loading the player.'}
51
+ {this.state.error?.message || "An unexpected error occurred while loading the player."}
53
52
  </p>
54
- <Button
55
- type="button"
56
- variant="secondary"
57
- onClick={this.handleRetry}
58
- className="mt-2"
59
- >
53
+ <Button type="button" variant="secondary" onClick={this.handleRetry} className="mt-2">
60
54
  Try Again
61
55
  </Button>
62
56
  </div>
@@ -127,97 +127,109 @@ const SeekBar: React.FC<SeekBarProps> = ({
127
127
 
128
128
  // Calculate time from mouse position
129
129
  // For live: maps position to time within DVR window
130
- const getTimeFromPosition = useCallback((clientX: number): number => {
131
- if (!trackRef.current) return 0;
132
- const rect = trackRef.current.getBoundingClientRect();
133
- const x = clientX - rect.left;
134
- const percent = Math.min(1, Math.max(0, x / rect.width));
130
+ const getTimeFromPosition = useCallback(
131
+ (clientX: number): number => {
132
+ if (!trackRef.current) return 0;
133
+ const rect = trackRef.current.getBoundingClientRect();
134
+ const x = clientX - rect.left;
135
+ const percent = Math.min(1, Math.max(0, x / rect.width));
135
136
 
136
- // Live with valid seekable window
137
- if (isLive && Number.isFinite(seekableWindow) && seekableWindow > 0) {
138
- return seekableStart + (percent * seekableWindow);
139
- }
137
+ // Live with valid seekable window
138
+ if (isLive && Number.isFinite(seekableWindow) && seekableWindow > 0) {
139
+ return seekableStart + percent * seekableWindow;
140
+ }
140
141
 
141
- // VOD with finite duration
142
- if (Number.isFinite(duration) && duration > 0) {
143
- return percent * duration;
144
- }
142
+ // VOD with finite duration
143
+ if (Number.isFinite(duration) && duration > 0) {
144
+ return percent * duration;
145
+ }
145
146
 
146
- // Fallback: If we have liveEdge, use it even if not marked as live
147
- // This handles cases where duration is Infinity but we have valid seekable data
148
- if (liveEdge !== undefined && Number.isFinite(liveEdge) && liveEdge > 0) {
149
- const start = Number.isFinite(seekableStart) ? seekableStart : 0;
150
- const window = liveEdge - start;
151
- if (window > 0) {
152
- return start + (percent * window);
147
+ // Fallback: If we have liveEdge, use it even if not marked as live
148
+ // This handles cases where duration is Infinity but we have valid seekable data
149
+ if (liveEdge !== undefined && Number.isFinite(liveEdge) && liveEdge > 0) {
150
+ const start = Number.isFinite(seekableStart) ? seekableStart : 0;
151
+ const window = liveEdge - start;
152
+ if (window > 0) {
153
+ return start + percent * window;
154
+ }
153
155
  }
154
- }
155
156
 
156
- // Last resort: use currentTime as a baseline
157
- return percent * (currentTime || 1);
158
- }, [duration, isLive, seekableStart, seekableWindow, liveEdge, currentTime]);
157
+ // Last resort: use currentTime as a baseline
158
+ return percent * (currentTime || 1);
159
+ },
160
+ [duration, isLive, seekableStart, seekableWindow, liveEdge, currentTime]
161
+ );
159
162
 
160
163
  // Handle mouse move for hover preview
161
- const handleMouseMove = useCallback((e: React.MouseEvent) => {
162
- if (!trackRef.current || disabled) return;
163
- const rect = trackRef.current.getBoundingClientRect();
164
- const x = e.clientX - rect.left;
165
- const percent = Math.min(1, Math.max(0, x / rect.width));
166
- setHoverPosition(percent * 100);
167
- setHoverTime(getTimeFromPosition(e.clientX));
168
- }, [disabled, getTimeFromPosition]);
164
+ const handleMouseMove = useCallback(
165
+ (e: React.MouseEvent) => {
166
+ if (!trackRef.current || disabled) return;
167
+ const rect = trackRef.current.getBoundingClientRect();
168
+ const x = e.clientX - rect.left;
169
+ const percent = Math.min(1, Math.max(0, x / rect.width));
170
+ setHoverPosition(percent * 100);
171
+ setHoverTime(getTimeFromPosition(e.clientX));
172
+ },
173
+ [disabled, getTimeFromPosition]
174
+ );
169
175
 
170
176
  // Handle click to seek
171
- const handleClick = useCallback((e: React.MouseEvent) => {
172
- if (disabled) return;
173
- if (!isLive && !Number.isFinite(duration)) return;
174
- const time = getTimeFromPosition(e.clientX);
175
- onSeek?.(time);
176
- setDragTime(null);
177
- dragTimeRef.current = null;
178
- }, [disabled, duration, isLive, getTimeFromPosition, onSeek]);
177
+ const handleClick = useCallback(
178
+ (e: React.MouseEvent) => {
179
+ if (disabled) return;
180
+ if (!isLive && !Number.isFinite(duration)) return;
181
+ const time = getTimeFromPosition(e.clientX);
182
+ onSeek?.(time);
183
+ setDragTime(null);
184
+ dragTimeRef.current = null;
185
+ },
186
+ [disabled, duration, isLive, getTimeFromPosition, onSeek]
187
+ );
179
188
 
180
189
  // Handle drag start
181
- const handleMouseDown = useCallback((e: React.MouseEvent) => {
182
- if (disabled) return;
183
- if (!isLive && !Number.isFinite(duration)) return;
184
- e.preventDefault();
185
- setIsDragging(true);
190
+ const handleMouseDown = useCallback(
191
+ (e: React.MouseEvent) => {
192
+ if (disabled) return;
193
+ if (!isLive && !Number.isFinite(duration)) return;
194
+ e.preventDefault();
195
+ setIsDragging(true);
196
+
197
+ const handleDragMove = (moveEvent: MouseEvent) => {
198
+ const time = getTimeFromPosition(moveEvent.clientX);
199
+ if (commitOnRelease) {
200
+ setDragTime(time);
201
+ dragTimeRef.current = time;
202
+ } else {
203
+ onSeek?.(time);
204
+ }
205
+ };
186
206
 
187
- const handleDragMove = (moveEvent: MouseEvent) => {
188
- const time = getTimeFromPosition(moveEvent.clientX);
207
+ const handleDragEnd = () => {
208
+ setIsDragging(false);
209
+ document.removeEventListener("mousemove", handleDragMove);
210
+ document.removeEventListener("mouseup", handleDragEnd);
211
+ const pending = dragTimeRef.current;
212
+ if (commitOnRelease && pending !== null) {
213
+ onSeek?.(pending);
214
+ setDragTime(null);
215
+ dragTimeRef.current = null;
216
+ }
217
+ };
218
+
219
+ document.addEventListener("mousemove", handleDragMove);
220
+ document.addEventListener("mouseup", handleDragEnd);
221
+
222
+ // Initial seek
223
+ const time = getTimeFromPosition(e.clientX);
189
224
  if (commitOnRelease) {
190
225
  setDragTime(time);
191
226
  dragTimeRef.current = time;
192
227
  } else {
193
228
  onSeek?.(time);
194
229
  }
195
- };
196
-
197
- const handleDragEnd = () => {
198
- setIsDragging(false);
199
- document.removeEventListener("mousemove", handleDragMove);
200
- document.removeEventListener("mouseup", handleDragEnd);
201
- const pending = dragTimeRef.current;
202
- if (commitOnRelease && pending !== null) {
203
- onSeek?.(pending);
204
- setDragTime(null);
205
- dragTimeRef.current = null;
206
- }
207
- };
208
-
209
- document.addEventListener("mousemove", handleDragMove);
210
- document.addEventListener("mouseup", handleDragEnd);
211
-
212
- // Initial seek
213
- const time = getTimeFromPosition(e.clientX);
214
- if (commitOnRelease) {
215
- setDragTime(time);
216
- dragTimeRef.current = time;
217
- } else {
218
- onSeek?.(time);
219
- }
220
- }, [disabled, duration, isLive, getTimeFromPosition, onSeek, commitOnRelease]);
230
+ },
231
+ [disabled, duration, isLive, getTimeFromPosition, onSeek, commitOnRelease]
232
+ );
221
233
 
222
234
  const showThumb = isHovering || isDragging;
223
235
  const canShowTooltip = isLive ? seekableWindow > 0 : Number.isFinite(duration);
@@ -231,23 +243,25 @@ const SeekBar: React.FC<SeekBarProps> = ({
231
243
  className
232
244
  )}
233
245
  onMouseEnter={() => !disabled && setIsHovering(true)}
234
- onMouseLeave={() => { setIsHovering(false); setIsDragging(false); }}
246
+ onMouseLeave={() => {
247
+ setIsHovering(false);
248
+ setIsDragging(false);
249
+ }}
235
250
  onMouseMove={handleMouseMove}
236
251
  onClick={handleClick}
237
252
  onMouseDown={handleMouseDown}
238
253
  role="slider"
239
254
  aria-label="Seek"
240
255
  aria-valuemin={isLive ? seekableStart : 0}
241
- aria-valuemax={isLive ? effectiveLiveEdge : (duration || 100)}
256
+ aria-valuemax={isLive ? effectiveLiveEdge : duration || 100}
242
257
  aria-valuenow={displayTime}
243
- aria-valuetext={isLive ? formatLiveTime(displayTime, effectiveLiveEdge) : formatTime(displayTime)}
258
+ aria-valuetext={
259
+ isLive ? formatLiveTime(displayTime, effectiveLiveEdge) : formatTime(displayTime)
260
+ }
244
261
  tabIndex={disabled ? -1 : 0}
245
262
  >
246
263
  {/* Track background */}
247
- <div className={cn(
248
- "fw-seek-track",
249
- isDragging && "fw-seek-track--active"
250
- )}>
264
+ <div className={cn("fw-seek-track", isDragging && "fw-seek-track--active")}>
251
265
  {/* Buffered segments - show actual buffered ranges */}
252
266
  {bufferedSegments.map((segment, index) => (
253
267
  <div
@@ -260,10 +274,7 @@ const SeekBar: React.FC<SeekBarProps> = ({
260
274
  />
261
275
  ))}
262
276
  {/* Playback progress */}
263
- <div
264
- className="fw-seek-progress"
265
- style={{ width: `${progressPercent}%` }}
266
- />
277
+ <div className="fw-seek-progress" style={{ width: `${progressPercent}%` }} />
267
278
  </div>
268
279
 
269
280
  {/* Thumb */}
@@ -277,10 +288,7 @@ const SeekBar: React.FC<SeekBarProps> = ({
277
288
 
278
289
  {/* Hover time tooltip */}
279
290
  {isHovering && !isDragging && canShowTooltip && (
280
- <div
281
- className="fw-seek-tooltip"
282
- style={{ left: `${hoverPosition}%` }}
283
- >
291
+ <div className="fw-seek-tooltip" style={{ left: `${hoverPosition}%` }}>
284
292
  {isLive ? formatLiveTime(hoverTime, effectiveLiveEdge) : formatTime(hoverTime)}
285
293
  </div>
286
294
  )}
@@ -89,23 +89,13 @@ const SkipIndicator: React.FC<SkipIndicatorProps> = ({
89
89
  };
90
90
 
91
91
  const RewindIcon: React.FC<{ className?: string }> = ({ className }) => (
92
- <svg
93
- viewBox="0 0 24 24"
94
- fill="currentColor"
95
- className={className}
96
- aria-hidden="true"
97
- >
92
+ <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
98
93
  <path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" />
99
94
  </svg>
100
95
  );
101
96
 
102
97
  const FastForwardIcon: React.FC<{ className?: string }> = ({ className }) => (
103
- <svg
104
- viewBox="0 0 24 24"
105
- fill="currentColor"
106
- className={className}
107
- aria-hidden="true"
108
- >
98
+ <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
109
99
  <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
110
100
  </svg>
111
101
  );
@@ -11,11 +11,7 @@ interface SpeedIndicatorProps {
11
11
  * Speed indicator overlay that appears when holding for fast-forward.
12
12
  * Shows the current playback speed (e.g., "2x") in a pill overlay.
13
13
  */
14
- const SpeedIndicator: React.FC<SpeedIndicatorProps> = ({
15
- isVisible,
16
- speed,
17
- className,
18
- }) => {
14
+ const SpeedIndicator: React.FC<SpeedIndicatorProps> = ({ isVisible, speed, className }) => {
19
15
  return (
20
16
  <div
21
17
  className={cn(
@@ -44,12 +40,7 @@ const SpeedIndicator: React.FC<SpeedIndicatorProps> = ({
44
40
 
45
41
  // Simple fast-forward icon
46
42
  const FastForwardIcon: React.FC<{ className?: string }> = ({ className }) => (
47
- <svg
48
- viewBox="0 0 24 24"
49
- fill="currentColor"
50
- className={className}
51
- aria-hidden="true"
52
- >
43
+ <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
53
44
  <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
54
45
  </svg>
55
46
  );
@@ -38,16 +38,15 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
38
38
  // Video element stats
39
39
  const video = videoElement;
40
40
  const currentRes = video ? `${video.videoWidth}x${video.videoHeight}` : "—";
41
- const buffered = video && video.buffered.length > 0
42
- ? (video.buffered.end(video.buffered.length - 1) - video.currentTime).toFixed(1)
43
- : "—";
41
+ const buffered =
42
+ video && video.buffered.length > 0
43
+ ? (video.buffered.end(video.buffered.length - 1) - video.currentTime).toFixed(1)
44
+ : "—";
44
45
  const playbackRate = video?.playbackRate?.toFixed(2) ?? "1.00";
45
46
 
46
47
  // Quality monitor stats
47
48
  const qualityScore = quality?.score?.toFixed(0) ?? "—";
48
- const bitrateKbps = quality?.bitrate
49
- ? `${(quality.bitrate / 1000).toFixed(0)} kbps`
50
- : "—";
49
+ const bitrateKbps = quality?.bitrate ? `${(quality.bitrate / 1000).toFixed(0)} kbps` : "—";
51
50
  const frameDropRate = quality?.frameDropRate?.toFixed(1) ?? "—";
52
51
  const stallCount = quality?.stallCount ?? 0;
53
52
  const latency = quality?.latency ? `${Math.round(quality.latency)} ms` : "—";
@@ -61,7 +60,7 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
61
60
  const deriveTracksFromMist = () => {
62
61
  const mistTracks = mistInfo?.meta?.tracks;
63
62
  if (!mistTracks) return undefined;
64
- return Object.values(mistTracks).map(t => ({
63
+ return Object.values(mistTracks).map((t) => ({
65
64
  type: t.type,
66
65
  codec: t.codec,
67
66
  width: t.width,
@@ -77,15 +76,17 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
77
76
  const formatTracks = () => {
78
77
  const tracks = metadata?.tracks ?? deriveTracksFromMist();
79
78
  if (!tracks?.length) return "—";
80
- return tracks.map(t => {
81
- if (t.type === "video") {
82
- const resolution = t.width && t.height ? `${t.width}x${t.height}` : "?";
83
- const bitrate = t.bitrate ? `${Math.round(t.bitrate / 1000)}kbps` : "?";
84
- return `${t.codec ?? "?"} ${resolution}@${bitrate}`;
85
- }
86
- const channels = t.channels ? `${t.channels}ch` : "?";
87
- return `${t.codec ?? "?"} ${channels}`;
88
- }).join(", ");
79
+ return tracks
80
+ .map((t) => {
81
+ if (t.type === "video") {
82
+ const resolution = t.width && t.height ? `${t.width}x${t.height}` : "?";
83
+ const bitrate = t.bitrate ? `${Math.round(t.bitrate / 1000)}kbps` : "?";
84
+ return `${t.codec ?? "?"} ${resolution}@${bitrate}`;
85
+ }
86
+ const channels = t.channels ? `${t.channels}ch` : "?";
87
+ return `${t.codec ?? "?"} ${channels}`;
88
+ })
89
+ .join(", ");
89
90
  };
90
91
 
91
92
  const mistType = mistInfo?.type ?? "—";
@@ -109,7 +110,10 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
109
110
  { label: "Status", value: streamStatus },
110
111
  { label: "Tracks", value: formatTracks() },
111
112
  { label: "Mist Type", value: mistType },
112
- { label: "Mist Buffer Window", value: mistBufferWindow != null ? String(mistBufferWindow) : "—" },
113
+ {
114
+ label: "Mist Buffer Window",
115
+ value: mistBufferWindow != null ? String(mistBufferWindow) : "—",
116
+ },
113
117
  { label: "Mist Lastms", value: mistLastMs != null ? String(mistLastMs) : "—" },
114
118
  { label: "Mist Unixoffset", value: mistUnixOffset != null ? String(mistUnixOffset) : "—" },
115
119
  ];
@@ -137,20 +141,25 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
137
141
  "max-w-[320px] max-h-[80%] overflow-auto",
138
142
  "shadow-lg"
139
143
  )}
140
- style={{ backgroundColor: '#000000' }} // Inline fallback for opaque background
144
+ style={{ backgroundColor: "#000000" }} // Inline fallback for opaque background
141
145
  >
142
146
  {/* Header */}
143
147
  <div className="flex items-center justify-between px-3 py-2 border-b border-white/10">
144
- <span className="text-white/70 text-[10px] uppercase tracking-wider">
145
- Stats Overlay
146
- </span>
148
+ <span className="text-white/70 text-[10px] uppercase tracking-wider">Stats Overlay</span>
147
149
  <button
148
150
  type="button"
149
151
  onClick={onClose}
150
152
  className="text-white/50 hover:text-white transition-colors p-1 -mr-1"
151
153
  aria-label="Close stats panel"
152
154
  >
153
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
155
+ <svg
156
+ width="12"
157
+ height="12"
158
+ viewBox="0 0 12 12"
159
+ fill="none"
160
+ stroke="currentColor"
161
+ strokeWidth="1.5"
162
+ >
154
163
  <path d="M2 2l8 8M10 2l-8 8" />
155
164
  </svg>
156
165
  </button>