@livepeer-frameworks/player-react 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 (88) hide show
  1. package/dist/cjs/index.js +2 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +2 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/types/components/DevModePanel.d.ts +47 -0
  6. package/dist/types/components/DvdLogo.d.ts +4 -0
  7. package/dist/types/components/Icons.d.ts +33 -0
  8. package/dist/types/components/IdleScreen.d.ts +16 -0
  9. package/dist/types/components/LoadingScreen.d.ts +6 -0
  10. package/dist/types/components/LogoOverlay.d.ts +11 -0
  11. package/dist/types/components/Player.d.ts +11 -0
  12. package/dist/types/components/PlayerControls.d.ts +60 -0
  13. package/dist/types/components/PlayerErrorBoundary.d.ts +23 -0
  14. package/dist/types/components/SeekBar.d.ts +33 -0
  15. package/dist/types/components/SkipIndicator.d.ts +14 -0
  16. package/dist/types/components/SpeedIndicator.d.ts +12 -0
  17. package/dist/types/components/StatsPanel.d.ts +31 -0
  18. package/dist/types/components/StreamStateOverlay.d.ts +24 -0
  19. package/dist/types/components/SubtitleRenderer.d.ts +69 -0
  20. package/dist/types/components/ThumbnailOverlay.d.ts +4 -0
  21. package/dist/types/components/TitleOverlay.d.ts +13 -0
  22. package/dist/types/components/players/DashJsPlayer.d.ts +18 -0
  23. package/dist/types/components/players/HlsJsPlayer.d.ts +18 -0
  24. package/dist/types/components/players/MewsWsPlayer/index.d.ts +18 -0
  25. package/dist/types/components/players/MistPlayer.d.ts +20 -0
  26. package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +20 -0
  27. package/dist/types/components/players/NativePlayer.d.ts +19 -0
  28. package/dist/types/components/players/VideoJsPlayer.d.ts +18 -0
  29. package/dist/types/context/PlayerContext.d.ts +40 -0
  30. package/dist/types/context/index.d.ts +5 -0
  31. package/dist/types/hooks/useMetaTrack.d.ts +54 -0
  32. package/dist/types/hooks/usePlaybackQuality.d.ts +42 -0
  33. package/dist/types/hooks/usePlayerController.d.ts +163 -0
  34. package/dist/types/hooks/usePlayerSelection.d.ts +47 -0
  35. package/dist/types/hooks/useStreamState.d.ts +27 -0
  36. package/dist/types/hooks/useTelemetry.d.ts +57 -0
  37. package/dist/types/hooks/useViewerEndpoints.d.ts +14 -0
  38. package/dist/types/index.d.ts +33 -0
  39. package/dist/types/types.d.ts +94 -0
  40. package/dist/types/ui/badge.d.ts +9 -0
  41. package/dist/types/ui/button.d.ts +11 -0
  42. package/dist/types/ui/context-menu.d.ts +27 -0
  43. package/dist/types/ui/select.d.ts +10 -0
  44. package/dist/types/ui/slider.d.ts +13 -0
  45. package/package.json +71 -0
  46. package/src/assets/logomark.svg +56 -0
  47. package/src/components/DevModePanel.tsx +822 -0
  48. package/src/components/DvdLogo.tsx +201 -0
  49. package/src/components/Icons.tsx +282 -0
  50. package/src/components/IdleScreen.tsx +664 -0
  51. package/src/components/LoadingScreen.tsx +710 -0
  52. package/src/components/LogoOverlay.tsx +75 -0
  53. package/src/components/Player.tsx +419 -0
  54. package/src/components/PlayerControls.tsx +820 -0
  55. package/src/components/PlayerErrorBoundary.tsx +70 -0
  56. package/src/components/SeekBar.tsx +291 -0
  57. package/src/components/SkipIndicator.tsx +113 -0
  58. package/src/components/SpeedIndicator.tsx +57 -0
  59. package/src/components/StatsPanel.tsx +150 -0
  60. package/src/components/StreamStateOverlay.tsx +200 -0
  61. package/src/components/SubtitleRenderer.tsx +235 -0
  62. package/src/components/ThumbnailOverlay.tsx +90 -0
  63. package/src/components/TitleOverlay.tsx +48 -0
  64. package/src/components/players/DashJsPlayer.tsx +56 -0
  65. package/src/components/players/HlsJsPlayer.tsx +56 -0
  66. package/src/components/players/MewsWsPlayer/index.tsx +56 -0
  67. package/src/components/players/MistPlayer.tsx +60 -0
  68. package/src/components/players/MistWebRTCPlayer/index.tsx +59 -0
  69. package/src/components/players/NativePlayer.tsx +58 -0
  70. package/src/components/players/VideoJsPlayer.tsx +56 -0
  71. package/src/context/PlayerContext.tsx +71 -0
  72. package/src/context/index.ts +11 -0
  73. package/src/global.d.ts +4 -0
  74. package/src/hooks/useMetaTrack.ts +187 -0
  75. package/src/hooks/usePlaybackQuality.ts +126 -0
  76. package/src/hooks/usePlayerController.ts +525 -0
  77. package/src/hooks/usePlayerSelection.ts +117 -0
  78. package/src/hooks/useStreamState.ts +381 -0
  79. package/src/hooks/useTelemetry.ts +138 -0
  80. package/src/hooks/useViewerEndpoints.ts +120 -0
  81. package/src/index.tsx +75 -0
  82. package/src/player.css +2 -0
  83. package/src/types.ts +135 -0
  84. package/src/ui/badge.tsx +27 -0
  85. package/src/ui/button.tsx +47 -0
  86. package/src/ui/context-menu.tsx +193 -0
  87. package/src/ui/select.tsx +105 -0
  88. package/src/ui/slider.tsx +67 -0
@@ -0,0 +1,201 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { DvdLogoProps } from "../types";
3
+
4
+ type Point = { top: number; left: number };
5
+ type Velocity = { x: number; y: number };
6
+ type Size = { width: number; height: number };
7
+
8
+ const ORIGINAL_WIDTH = 153;
9
+ const ORIGINAL_HEIGHT = 69;
10
+ const ASPECT_RATIO = ORIGINAL_WIDTH / ORIGINAL_HEIGHT;
11
+
12
+ const COLORS = [
13
+ "#7aa2f7",
14
+ "#bb9af7",
15
+ "#9ece6a",
16
+ "#73daca",
17
+ "#7dcfff",
18
+ "#f7768e",
19
+ "#e0af68",
20
+ "#2ac3de",
21
+ ];
22
+
23
+ const pickNextColor = (current?: string): string => {
24
+ if (COLORS.length === 0) {
25
+ return current ?? "#ffffff";
26
+ }
27
+
28
+ if (COLORS.length === 1) {
29
+ return COLORS[0];
30
+ }
31
+
32
+ let next: string;
33
+ do {
34
+ next = COLORS[Math.floor(Math.random() * COLORS.length)];
35
+ } while (next === current);
36
+
37
+ return next;
38
+ };
39
+
40
+ const DvdLogo: React.FC<DvdLogoProps> = ({ parentRef, scale = 0.15 }) => {
41
+ const [position, setPosition] = useState<Point>({ top: 0, left: 0 });
42
+ const positionRef = useRef<Point>(position);
43
+ useEffect(() => {
44
+ positionRef.current = position;
45
+ }, [position]);
46
+
47
+ const [dimensions, setDimensions] = useState<Size>({
48
+ width: ORIGINAL_WIDTH,
49
+ height: ORIGINAL_HEIGHT,
50
+ });
51
+ const dimensionsRef = useRef<Size>(dimensions);
52
+ useEffect(() => {
53
+ dimensionsRef.current = dimensions;
54
+ }, [dimensions]);
55
+
56
+ const velocityRef = useRef<Velocity>({ x: 1.8, y: 1.6 });
57
+ const [color, setColor] = useState<string>(pickNextColor());
58
+
59
+ const recalculateDimensions = React.useCallback(() => {
60
+ const parent = parentRef.current;
61
+ if (!parent) {
62
+ return;
63
+ }
64
+
65
+ const parentWidth = parent.clientWidth;
66
+ const parentHeight = parent.clientHeight;
67
+ if (parentWidth === 0 || parentHeight === 0) {
68
+ return;
69
+ }
70
+
71
+ const maxWidth = parentWidth * scale;
72
+ const maxHeight = parentHeight * scale;
73
+
74
+ let width = maxWidth;
75
+ let height = width / ASPECT_RATIO;
76
+
77
+ if (height > maxHeight) {
78
+ height = maxHeight;
79
+ width = height * ASPECT_RATIO;
80
+ }
81
+
82
+ const nextDimensions: Size = {
83
+ width: Math.max(20, width),
84
+ height: Math.max(20, height),
85
+ };
86
+
87
+ dimensionsRef.current = nextDimensions;
88
+ setDimensions(nextDimensions);
89
+
90
+ const maxTop = Math.max(0, parentHeight - nextDimensions.height);
91
+ const maxLeft = Math.max(0, parentWidth - nextDimensions.width);
92
+
93
+ const startPosition: Point = {
94
+ top: Math.random() * maxTop,
95
+ left: Math.random() * maxLeft,
96
+ };
97
+
98
+ positionRef.current = startPosition;
99
+ setPosition(startPosition);
100
+
101
+ const baseSpeed = Math.max(1.2, Math.min(nextDimensions.width, nextDimensions.height) / 70);
102
+ velocityRef.current = {
103
+ x: baseSpeed * (Math.random() > 0.5 ? 1 : -1),
104
+ y: baseSpeed * (Math.random() > 0.5 ? 1 : -1),
105
+ };
106
+ }, [parentRef, scale]);
107
+
108
+ useEffect(() => {
109
+ if (!parentRef.current) {
110
+ return;
111
+ }
112
+
113
+ recalculateDimensions();
114
+
115
+ if (typeof ResizeObserver !== "undefined") {
116
+ const observer = new ResizeObserver(() => recalculateDimensions());
117
+ observer.observe(parentRef.current);
118
+ return () => observer.disconnect();
119
+ }
120
+
121
+ const onResize = () => recalculateDimensions();
122
+ window.addEventListener("resize", onResize);
123
+ return () => window.removeEventListener("resize", onResize);
124
+ }, [parentRef, recalculateDimensions]);
125
+
126
+ useEffect(() => {
127
+ let animationFrame: number;
128
+ let lastTimestamp = performance.now();
129
+
130
+ const animate = (timestamp: number) => {
131
+ const parent = parentRef.current;
132
+ const dims = dimensionsRef.current;
133
+
134
+ if (!parent || dims.width === 0 || dims.height === 0) {
135
+ animationFrame = requestAnimationFrame(animate);
136
+ return;
137
+ }
138
+
139
+ const deltaMs = timestamp - lastTimestamp;
140
+ lastTimestamp = timestamp;
141
+ const speedMultiplier = Math.min(deltaMs / 16, 2);
142
+
143
+ const maxTop = parent.clientHeight - dims.height;
144
+ const maxLeft = parent.clientWidth - dims.width;
145
+
146
+ let { top, left } = positionRef.current;
147
+ let { x, y } = velocityRef.current;
148
+ let bounced = false;
149
+
150
+ top += y * speedMultiplier;
151
+ left += x * speedMultiplier;
152
+
153
+ if (top <= 0 || top >= maxTop) {
154
+ y = -y;
155
+ top = Math.max(0, Math.min(maxTop, top));
156
+ bounced = true;
157
+ }
158
+
159
+ if (left <= 0 || left >= maxLeft) {
160
+ x = -x;
161
+ left = Math.max(0, Math.min(maxLeft, left));
162
+ bounced = true;
163
+ }
164
+
165
+ velocityRef.current = { x, y };
166
+ const nextPosition = { top, left };
167
+ positionRef.current = nextPosition;
168
+ setPosition(nextPosition);
169
+
170
+ if (bounced) {
171
+ setColor((current) => pickNextColor(current));
172
+ }
173
+
174
+ animationFrame = requestAnimationFrame(animate);
175
+ };
176
+
177
+ animationFrame = requestAnimationFrame(animate);
178
+
179
+ return () => cancelAnimationFrame(animationFrame);
180
+ }, [parentRef]);
181
+
182
+ return (
183
+ <div
184
+ className="fw-player-dvd"
185
+ style={{
186
+ top: `${position.top}px`,
187
+ left: `${position.left}px`,
188
+ width: `${dimensions.width}px`,
189
+ height: `${dimensions.height}px`,
190
+ }}
191
+ >
192
+ <svg width="100%" height="100%" viewBox="0 0 153 69" fill={color} className="select-none">
193
+ <g>
194
+ <path d="M140.186,63.52h-1.695l-0.692,5.236h-0.847l0.77-5.236h-1.693l0.076-0.694h4.158L140.186,63.52L140.186,63.52z M146.346,68.756h-0.848v-4.545l0,0l-2.389,4.545l-1-4.545l0,0l-1.462,4.545h-0.771l1.924-5.931h0.695l0.924,4.006l2.078-4.006 h0.848V68.756L146.346,68.756z M126.027,0.063H95.352c0,0-8.129,9.592-9.654,11.434c-8.064,9.715-9.523,12.32-9.779,13.02 c0.063-0.699-0.256-3.304-3.686-13.148C71.282,8.7,68.359,0.062,68.359,0.062H57.881V0L32.35,0.063H13.169l-1.97,8.131 l14.543,0.062h3.365c9.336,0,15.055,3.747,13.467,10.354c-1.717,7.24-9.91,10.416-18.545,10.416h-3.24l4.191-17.783H10.502 L4.34,37.219h20.578c15.432,0,30.168-8.13,32.709-18.608c0.508-1.906,0.443-6.67-0.764-9.527c0-0.127-0.063-0.191-0.127-0.444 c-0.064-0.063-0.127-0.509,0.127-0.571c0.128-0.062,0.383,0.189,0.445,0.254c0.127,0.317,0.19,0.57,0.19,0.57l13.083,36.965 l33.344-37.6h14.1h3.365c9.337,0,15.055,3.747,13.528,10.354c-1.778,7.24-9.972,10.416-18.608,10.416h-3.238l4.191-17.783h-14.481 l-6.159,25.976h20.576c15.434,0,30.232-8.13,32.709-18.608C152.449,8.193,141.523,0.063,126.027,0.063L126.027,0.063z M71.091,45.981c-39.123,0-70.816,4.512-70.816,10.035c0,5.59,31.693,10.034,70.816,10.034c39.121,0,70.877-4.444,70.877-10.034 C141.968,50.493,110.212,45.981,71.091,45.981L71.091,45.981z M68.55,59.573c-8.956,0-16.196-1.523-16.196-3.365 c0-1.84,7.239-3.303,16.196-3.303c8.955,0,16.195,1.463,16.195,3.303C84.745,58.050,77.505,59.573,68.55,59.573L68.55,59.573z" />
195
+ </g>
196
+ </svg>
197
+ </div>
198
+ );
199
+ };
200
+
201
+ export default DvdLogo;
@@ -0,0 +1,282 @@
1
+ import React from 'react';
2
+
3
+ interface IconProps {
4
+ size?: number;
5
+ color?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export const PlayIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
10
+ <svg
11
+ width={size}
12
+ height={size}
13
+ viewBox="0 0 24 24"
14
+ fill="none"
15
+ className={className}
16
+ aria-hidden="true"
17
+ >
18
+ <path
19
+ d="M8 5v14l11-7z"
20
+ fill={color}
21
+ />
22
+ </svg>
23
+ );
24
+
25
+ export const PauseIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
26
+ <svg
27
+ width={size}
28
+ height={size}
29
+ viewBox="0 0 24 24"
30
+ fill="none"
31
+ className={className}
32
+ aria-hidden="true"
33
+ >
34
+ <rect x="6" y="4" width="4" height="16" fill={color} />
35
+ <rect x="14" y="4" width="4" height="16" fill={color} />
36
+ </svg>
37
+ );
38
+
39
+ export const SkipBackIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
40
+ <svg
41
+ width={size}
42
+ height={size}
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ className={className}
46
+ aria-hidden="true"
47
+ >
48
+ {/* Circular rewind arrow */}
49
+ <path
50
+ d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"
51
+ fill={color}
52
+ />
53
+ <text x="12" y="15" fontSize="7" fontWeight="bold" fill={color} textAnchor="middle">10</text>
54
+ </svg>
55
+ );
56
+
57
+ export const SkipForwardIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
58
+ <svg
59
+ width={size}
60
+ height={size}
61
+ viewBox="0 0 24 24"
62
+ fill="none"
63
+ className={className}
64
+ aria-hidden="true"
65
+ >
66
+ {/* Circular forward arrow */}
67
+ <path
68
+ d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"
69
+ fill={color}
70
+ />
71
+ <text x="12" y="15" fontSize="7" fontWeight="bold" fill={color} textAnchor="middle">10</text>
72
+ </svg>
73
+ );
74
+
75
+ export const VolumeUpIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
76
+ <svg
77
+ width={size}
78
+ height={size}
79
+ viewBox="0 0 24 24"
80
+ fill="none"
81
+ className={className}
82
+ aria-hidden="true"
83
+ >
84
+ <polygon points="11,5 6,9 2,9 2,15 6,15 11,19" fill={color} />
85
+ <path
86
+ d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"
87
+ stroke={color}
88
+ strokeWidth="2"
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ />
92
+ </svg>
93
+ );
94
+
95
+ export const VolumeOffIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
96
+ <svg
97
+ width={size}
98
+ height={size}
99
+ viewBox="0 0 24 24"
100
+ fill="none"
101
+ className={className}
102
+ aria-hidden="true"
103
+ >
104
+ <polygon points="11,5 6,9 2,9 2,15 6,15 11,19" fill={color} />
105
+ <line x1="23" y1="9" x2="17" y2="15" stroke={color} strokeWidth="2" strokeLinecap="round" />
106
+ <line x1="17" y1="9" x2="23" y2="15" stroke={color} strokeWidth="2" strokeLinecap="round" />
107
+ </svg>
108
+ );
109
+
110
+ export const FullscreenIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
111
+ <svg
112
+ width={size}
113
+ height={size}
114
+ viewBox="0 0 24 24"
115
+ fill="none"
116
+ className={className}
117
+ aria-hidden="true"
118
+ >
119
+ <path
120
+ d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3m8 0h3a2 2 0 0 0 2-2v-3"
121
+ stroke={color}
122
+ strokeWidth="2"
123
+ strokeLinecap="round"
124
+ strokeLinejoin="round"
125
+ />
126
+ </svg>
127
+ );
128
+
129
+ export const FullscreenExitIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
130
+ <svg
131
+ width={size}
132
+ height={size}
133
+ viewBox="0 0 24 24"
134
+ fill="none"
135
+ className={className}
136
+ aria-hidden="true"
137
+ >
138
+ <path
139
+ d="M8 3v3a2 2 0 0 1-2 2H3M21 8h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3M16 21v-3a2 2 0 0 1 2-2h3"
140
+ stroke={color}
141
+ strokeWidth="2"
142
+ strokeLinecap="round"
143
+ strokeLinejoin="round"
144
+ />
145
+ </svg>
146
+ );
147
+
148
+ export const PictureInPictureIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
149
+ <svg
150
+ width={size}
151
+ height={size}
152
+ viewBox="0 0 24 24"
153
+ fill="none"
154
+ className={className}
155
+ aria-hidden="true"
156
+ >
157
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2" stroke={color} strokeWidth="2" fill="none" />
158
+ <rect x="8" y="10" width="10" height="6" rx="1" ry="1" fill={color} />
159
+ </svg>
160
+ );
161
+
162
+ export const ClosedCaptionsIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
163
+ <svg
164
+ width={size}
165
+ height={size}
166
+ viewBox="0 0 24 24"
167
+ fill="none"
168
+ className={className}
169
+ aria-hidden="true"
170
+ >
171
+ <rect x="2" y="4" width="20" height="16" rx="2" ry="2" stroke={color} strokeWidth="2" fill="none" />
172
+ <path
173
+ d="M8 10c0-.6.4-1 1-1h1c.6 0 1 .4 1 1v4c0 .6-.4 1-1 1H9c-.6 0-1-.4-1-1v-4zM14 10c0-.6.4-1 1-1h1c.6 0 1 .4 1 1v4c0 .6-.4 1-1 1h-1c-.6 0-1-.4-1-1v-4z"
174
+ fill={color}
175
+ />
176
+ </svg>
177
+ );
178
+
179
+ export const LiveIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
180
+ <svg
181
+ width={size}
182
+ height={size}
183
+ viewBox="0 0 24 24"
184
+ fill="none"
185
+ className={className}
186
+ aria-hidden="true"
187
+ >
188
+ <circle cx="12" cy="12" r="3" fill={color} />
189
+ <path
190
+ d="M12 1v6M12 17v6M4.22 4.22l4.24 4.24M15.54 15.54l4.24 4.24M1 12h6M17 12h6M4.22 19.78l4.24-4.24M15.54 8.46l4.24-4.24"
191
+ stroke={color}
192
+ strokeWidth="2"
193
+ strokeLinecap="round"
194
+ />
195
+ </svg>
196
+ );
197
+
198
+ export const SettingsIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
199
+ <svg
200
+ width={size}
201
+ height={size}
202
+ viewBox="0 0 24 24"
203
+ fill="none"
204
+ className={className}
205
+ aria-hidden="true"
206
+ >
207
+ <path
208
+ d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
209
+ stroke={color}
210
+ strokeWidth="2"
211
+ strokeLinecap="round"
212
+ strokeLinejoin="round"
213
+ />
214
+ <path
215
+ d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1Z"
216
+ stroke={color}
217
+ strokeWidth="2"
218
+ strokeLinecap="round"
219
+ strokeLinejoin="round"
220
+ />
221
+ </svg>
222
+ );
223
+
224
+ // Compound PlayPause icon that switches based on state
225
+ interface PlayPauseIconProps extends IconProps {
226
+ isPlaying?: boolean;
227
+ }
228
+
229
+ export const PlayPauseIcon: React.FC<PlayPauseIconProps> = ({ isPlaying, ...props }) => {
230
+ return isPlaying ? <PauseIcon {...props} /> : <PlayIcon {...props} />;
231
+ };
232
+
233
+ // Volume icon that switches based on mute state
234
+ interface VolumeIconProps extends IconProps {
235
+ isMuted?: boolean;
236
+ }
237
+
238
+ export const VolumeIcon: React.FC<VolumeIconProps> = ({ isMuted, ...props }) => {
239
+ return isMuted ? <VolumeOffIcon {...props} /> : <VolumeUpIcon {...props} />;
240
+ };
241
+
242
+ // Fullscreen icon that switches based on fullscreen state
243
+ interface FullscreenToggleIconProps extends IconProps {
244
+ isFullscreen?: boolean;
245
+ }
246
+
247
+ export const FullscreenToggleIcon: React.FC<FullscreenToggleIconProps> = ({ isFullscreen, ...props }) => {
248
+ return isFullscreen ? <FullscreenExitIcon {...props} /> : <FullscreenIcon {...props} />;
249
+ };
250
+
251
+ // Stats icon (bar chart style - recognizable for "stats for nerds")
252
+ export const StatsIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
253
+ <svg
254
+ width={size}
255
+ height={size}
256
+ viewBox="0 0 24 24"
257
+ fill="none"
258
+ className={className}
259
+ aria-hidden="true"
260
+ >
261
+ <rect x="4" y="13" width="4" height="7" fill={color} />
262
+ <rect x="10" y="9" width="4" height="11" fill={color} />
263
+ <rect x="16" y="4" width="4" height="16" fill={color} />
264
+ </svg>
265
+ );
266
+
267
+ // Seek to live/end icon (skip-to-end style: play triangle + bar)
268
+ export const SeekToLiveIcon: React.FC<IconProps> = ({ size = 16, color = 'currentColor', className = '' }) => (
269
+ <svg
270
+ width={size}
271
+ height={size}
272
+ viewBox="0 0 24 24"
273
+ fill="none"
274
+ className={className}
275
+ aria-hidden="true"
276
+ >
277
+ {/* Play triangle */}
278
+ <path d="M5 5v14l11-7z" fill={color} />
279
+ {/* End bar */}
280
+ <rect x="17" y="5" width="3" height="14" fill={color} />
281
+ </svg>
282
+ );