@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,13 +1,14 @@
1
- import React, { useState, useCallback } from "react";
1
+ import React, { useState, useCallback, useEffect } from "react";
2
2
  import IdleScreen from "./IdleScreen";
3
3
  import TitleOverlay from "./TitleOverlay";
4
4
  import StatsPanel from "./StatsPanel";
5
5
  import PlayerControls from "./PlayerControls";
6
6
  import DevModePanel from "./DevModePanel";
7
7
  import SpeedIndicator from "./SpeedIndicator";
8
- import SkipIndicator, { SkipDirection } from "./SkipIndicator";
8
+ import type { SkipDirection } from "./SkipIndicator";
9
+ import SkipIndicator from "./SkipIndicator";
9
10
  import { StatsIcon, SettingsIcon, PictureInPictureIcon } from "./Icons";
10
- import { PlayerProps } from "../types";
11
+ import type { PlayerProps } from "../types";
11
12
  import { usePlayerController } from "../hooks/usePlayerController";
12
13
  import { cn } from "@livepeer-frameworks/player-core";
13
14
  import type { PlaybackMode, EndpointInfo } from "@livepeer-frameworks/player-core";
@@ -28,7 +29,7 @@ const PlayerInner: React.FC<PlayerProps> = ({
28
29
  thumbnailUrl = null,
29
30
  options,
30
31
  endpoints: propsEndpoints,
31
- onStateChange
32
+ onStateChange,
32
33
  }) => {
33
34
  // ============================================================================
34
35
  // UI-only State (stays in wrapper)
@@ -38,7 +39,9 @@ const PlayerInner: React.FC<PlayerProps> = ({
38
39
  const [skipDirection, setSkipDirection] = useState<SkipDirection>(null);
39
40
 
40
41
  // Playback mode preference (persistent)
41
- const [devPlaybackMode, setDevPlaybackMode] = useState<PlaybackMode>(options?.playbackMode || 'auto');
42
+ const [devPlaybackMode, setDevPlaybackMode] = useState<PlaybackMode>(
43
+ options?.playbackMode || "auto"
44
+ );
42
45
 
43
46
  // ============================================================================
44
47
  // PlayerController Hook - ALL business logic
@@ -55,6 +58,7 @@ const PlayerInner: React.FC<PlayerProps> = ({
55
58
  setVolume,
56
59
  selectQuality,
57
60
  clearError,
61
+ dismissToast,
58
62
  retry,
59
63
  reload,
60
64
  jumpToLive,
@@ -78,31 +82,33 @@ const PlayerInner: React.FC<PlayerProps> = ({
78
82
  onStateChange?.(playerState);
79
83
  },
80
84
  onError: (error) => {
81
- console.warn('[Player] Error:', error);
85
+ console.warn("[Player] Error:", error);
82
86
  },
83
87
  });
84
88
 
85
89
  // ============================================================================
86
90
  // Dev Mode Callbacks
87
91
  // ============================================================================
88
- const handleDevSettingsChange = useCallback((settings: {
89
- forcePlayer?: string;
90
- forceType?: string;
91
- forceSource?: number;
92
- }) => {
93
- // One-shot selection - controller handles the state
94
- setDevModeOptions({
95
- forcePlayer: settings.forcePlayer,
96
- forceType: settings.forceType,
97
- forceSource: settings.forceSource,
98
- });
99
- }, [setDevModeOptions]);
92
+ const handleDevSettingsChange = useCallback(
93
+ (settings: { forcePlayer?: string; forceType?: string; forceSource?: number }) => {
94
+ // One-shot selection - controller handles the state
95
+ setDevModeOptions({
96
+ forcePlayer: settings.forcePlayer,
97
+ forceType: settings.forceType,
98
+ forceSource: settings.forceSource,
99
+ });
100
+ },
101
+ [setDevModeOptions]
102
+ );
100
103
 
101
- const handleModeChange = useCallback((mode: PlaybackMode) => {
102
- setDevPlaybackMode(mode);
103
- // Mode is a persistent preference
104
- setDevModeOptions({ playbackMode: mode });
105
- }, [setDevModeOptions]);
104
+ const handleModeChange = useCallback(
105
+ (mode: PlaybackMode) => {
106
+ setDevPlaybackMode(mode);
107
+ // Mode is a persistent preference
108
+ setDevModeOptions({ playbackMode: mode });
109
+ },
110
+ [setDevModeOptions]
111
+ );
106
112
 
107
113
  const handleReload = useCallback(() => {
108
114
  clearError();
@@ -110,7 +116,7 @@ const PlayerInner: React.FC<PlayerProps> = ({
110
116
  }, [clearError, reload]);
111
117
 
112
118
  const handleStatsToggle = useCallback(() => {
113
- setIsStatsOpen(prev => !prev);
119
+ setIsStatsOpen((prev) => !prev);
114
120
  }, []);
115
121
 
116
122
  // Clear skip indicator after animation
@@ -118,28 +124,42 @@ const PlayerInner: React.FC<PlayerProps> = ({
118
124
  setSkipDirection(null);
119
125
  }, []);
120
126
 
127
+ // Auto-dismiss toast after 3 seconds
128
+ useEffect(() => {
129
+ if (!state.toast) return;
130
+ const timer = setTimeout(() => {
131
+ dismissToast();
132
+ }, 3000);
133
+ return () => clearTimeout(timer);
134
+ }, [state.toast, dismissToast]);
135
+
121
136
  // ============================================================================
122
137
  // Derived Values
123
138
  // ============================================================================
124
139
  const primaryEndpoint = state.endpoints?.primary as EndpointInfo | undefined;
125
- const isLegacyPlayer = state.currentPlayerInfo?.shortname === 'mist-legacy';
140
+ const isLegacyPlayer = state.currentPlayerInfo?.shortname === "mist-legacy";
126
141
  const useStockControls = options?.stockControls === true || isLegacyPlayer;
127
142
 
128
143
  // Title overlay visibility: show on hover or when paused
129
- const showTitleOverlay = (state.isHovering || state.isPaused) &&
130
- !state.shouldShowIdleScreen && !state.isBuffering && !state.error;
144
+ const showTitleOverlay =
145
+ (state.isHovering || state.isPaused) &&
146
+ !state.shouldShowIdleScreen &&
147
+ !state.isBuffering &&
148
+ !state.error;
131
149
 
132
150
  // Buffering spinner: only during active playback
133
- const showBufferingSpinner = !state.shouldShowIdleScreen &&
134
- state.isBuffering && !state.error && state.hasPlaybackStarted;
151
+ const showBufferingSpinner =
152
+ !state.shouldShowIdleScreen && state.isBuffering && !state.error && state.hasPlaybackStarted;
135
153
 
136
154
  // ============================================================================
137
155
  // Waiting for Endpoint (shown as overlay, not early return)
138
156
  // ============================================================================
139
- const showWaitingForEndpoint = !state.endpoints?.primary && state.state !== 'booting';
157
+ const showWaitingForEndpoint = !state.endpoints?.primary && state.state !== "booting";
140
158
  const waitingMessage = options?.gatewayUrl
141
- ? (state.state === 'gateway_loading' ? 'Resolving viewing endpoint...' : 'Waiting for endpoint...')
142
- : 'Waiting for endpoint...';
159
+ ? state.state === "gateway_loading"
160
+ ? "Resolving viewing endpoint..."
161
+ : "Waiting for endpoint..."
162
+ : "Waiting for endpoint...";
143
163
 
144
164
  // ============================================================================
145
165
  // Render
@@ -159,10 +179,7 @@ const PlayerInner: React.FC<PlayerProps> = ({
159
179
  onMouseMove={handleMouseMove}
160
180
  >
161
181
  {/* Player area */}
162
- <div className={cn(
163
- "relative",
164
- options?.devMode ? "flex-1 min-w-0" : "w-full h-full"
165
- )}>
182
+ <div className={cn("relative", options?.devMode ? "flex-1 min-w-0" : "w-full h-full")}>
166
183
  {/* Video container - PlayerController attaches here */}
167
184
  <div ref={containerRef} className="fw-player-container" />
168
185
 
@@ -207,9 +224,7 @@ const PlayerInner: React.FC<PlayerProps> = ({
207
224
  )}
208
225
 
209
226
  {/* Speed indicator */}
210
- {state.isHoldingSpeed && (
211
- <SpeedIndicator isVisible={true} speed={state.holdSpeed} />
212
- )}
227
+ {state.isHoldingSpeed && <SpeedIndicator isVisible={true} speed={state.holdSpeed} />}
213
228
 
214
229
  {/* Skip indicator */}
215
230
  <SkipIndicator
@@ -219,15 +234,13 @@ const PlayerInner: React.FC<PlayerProps> = ({
219
234
  />
220
235
 
221
236
  {/* Waiting for endpoint overlay */}
222
- {showWaitingForEndpoint && (
223
- <IdleScreen status="OFFLINE" message={waitingMessage} />
224
- )}
237
+ {showWaitingForEndpoint && <IdleScreen status="OFFLINE" message={waitingMessage} />}
225
238
 
226
239
  {/* Idle screen */}
227
240
  {!showWaitingForEndpoint && state.shouldShowIdleScreen && (
228
241
  <IdleScreen
229
242
  status={state.isEffectivelyLive ? state.streamState?.status : undefined}
230
- message={state.isEffectivelyLive ? state.streamState?.message : 'Loading video...'}
243
+ message={state.isEffectivelyLive ? state.streamState?.message : "Loading video..."}
231
244
  percentage={state.isEffectivelyLive ? state.streamState?.percentage : undefined}
232
245
  />
233
246
  )}
@@ -253,21 +266,29 @@ const PlayerInner: React.FC<PlayerProps> = ({
253
266
  aria-live="assertive"
254
267
  className={cn(
255
268
  "fw-error-overlay",
256
- state.isPassiveError ? "fw-error-overlay--passive" : "fw-error-overlay--fullscreen"
269
+ state.isPassiveError
270
+ ? "fw-error-overlay--passive"
271
+ : "fw-error-overlay--fullscreen"
257
272
  )}
258
273
  >
259
- <div className={cn(
260
- "fw-error-popup",
261
- state.isPassiveError ? "fw-error-popup--passive" : "fw-error-popup--fullscreen"
262
- )}>
263
- <div className={cn(
264
- "fw-error-header",
265
- state.isPassiveError ? "fw-error-header--warning" : "fw-error-header--error"
266
- )}>
267
- <span className={cn(
268
- "fw-error-title",
269
- state.isPassiveError ? "fw-error-title--warning" : "fw-error-title--error"
270
- )}>
274
+ <div
275
+ className={cn(
276
+ "fw-error-popup",
277
+ state.isPassiveError ? "fw-error-popup--passive" : "fw-error-popup--fullscreen"
278
+ )}
279
+ >
280
+ <div
281
+ className={cn(
282
+ "fw-error-header",
283
+ state.isPassiveError ? "fw-error-header--warning" : "fw-error-header--error"
284
+ )}
285
+ >
286
+ <span
287
+ className={cn(
288
+ "fw-error-title",
289
+ state.isPassiveError ? "fw-error-title--warning" : "fw-error-title--error"
290
+ )}
291
+ >
271
292
  {state.isPassiveError ? "Warning" : "Error"}
272
293
  </span>
273
294
  <button
@@ -277,7 +298,12 @@ const PlayerInner: React.FC<PlayerProps> = ({
277
298
  aria-label="Dismiss"
278
299
  >
279
300
  <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
280
- <path d="M9 3L3 9M3 3L9 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
301
+ <path
302
+ d="M9 3L3 9M3 3L9 9"
303
+ stroke="currentColor"
304
+ strokeWidth="1.5"
305
+ strokeLinecap="round"
306
+ />
281
307
  </svg>
282
308
  </button>
283
309
  </div>
@@ -289,7 +315,10 @@ const PlayerInner: React.FC<PlayerProps> = ({
289
315
  type="button"
290
316
  className="fw-error-btn"
291
317
  aria-label="Retry playback"
292
- onClick={() => { clearError(); retry(); }}
318
+ onClick={() => {
319
+ clearError();
320
+ retry();
321
+ }}
293
322
  >
294
323
  Retry
295
324
  </button>
@@ -298,6 +327,34 @@ const PlayerInner: React.FC<PlayerProps> = ({
298
327
  </div>
299
328
  )}
300
329
 
330
+ {/* Toast notification */}
331
+ {state.toast && (
332
+ <div
333
+ className="absolute bottom-20 left-1/2 -translate-x-1/2 z-30 animate-in fade-in slide-in-from-bottom-2 duration-200"
334
+ role="status"
335
+ aria-live="polite"
336
+ >
337
+ <div className="flex items-center gap-2 rounded-lg border border-white/10 bg-black/80 px-4 py-2 text-sm text-white shadow-lg backdrop-blur-sm">
338
+ <span>{state.toast.message}</span>
339
+ <button
340
+ type="button"
341
+ onClick={dismissToast}
342
+ className="ml-2 text-white/60 hover:text-white"
343
+ aria-label="Dismiss"
344
+ >
345
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
346
+ <path
347
+ d="M9 3L3 9M3 3L9 9"
348
+ stroke="currentColor"
349
+ strokeWidth="1.5"
350
+ strokeLinecap="round"
351
+ />
352
+ </svg>
353
+ </button>
354
+ </div>
355
+ </div>
356
+ )}
357
+
301
358
  {/* Player controls */}
302
359
  {!useStockControls && (
303
360
  <PlayerControls
@@ -376,7 +433,17 @@ const PlayerInner: React.FC<PlayerProps> = ({
376
433
  <span>Picture-in-Picture</span>
377
434
  </ContextMenuItem>
378
435
  <ContextMenuItem onClick={toggleLoop} className="gap-2">
379
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="opacity-70 flex-shrink-0">
436
+ <svg
437
+ width="14"
438
+ height="14"
439
+ viewBox="0 0 24 24"
440
+ fill="none"
441
+ stroke="currentColor"
442
+ strokeWidth="2"
443
+ strokeLinecap="round"
444
+ strokeLinejoin="round"
445
+ className="opacity-70 flex-shrink-0"
446
+ >
380
447
  <polyline points="17 1 21 5 17 9"></polyline>
381
448
  <path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
382
449
  <polyline points="7 23 3 19 7 15"></polyline>