@livepeer-frameworks/player-react 0.1.0 → 0.1.1

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 (41) hide show
  1. package/README.md +7 -9
  2. package/package.json +1 -1
  3. package/src/components/DevModePanel.tsx +244 -143
  4. package/src/components/Icons.tsx +105 -25
  5. package/src/components/IdleScreen.tsx +262 -128
  6. package/src/components/LoadingScreen.tsx +169 -151
  7. package/src/components/LogoOverlay.tsx +3 -6
  8. package/src/components/Player.tsx +84 -56
  9. package/src/components/PlayerControls.tsx +349 -256
  10. package/src/components/PlayerErrorBoundary.tsx +6 -13
  11. package/src/components/SeekBar.tsx +96 -88
  12. package/src/components/SkipIndicator.tsx +2 -12
  13. package/src/components/SpeedIndicator.tsx +2 -11
  14. package/src/components/StatsPanel.tsx +31 -22
  15. package/src/components/StreamStateOverlay.tsx +105 -49
  16. package/src/components/SubtitleRenderer.tsx +29 -29
  17. package/src/components/ThumbnailOverlay.tsx +5 -6
  18. package/src/components/TitleOverlay.tsx +2 -8
  19. package/src/components/players/DashJsPlayer.tsx +13 -11
  20. package/src/components/players/HlsJsPlayer.tsx +13 -11
  21. package/src/components/players/MewsWsPlayer/index.tsx +13 -11
  22. package/src/components/players/MistPlayer.tsx +13 -11
  23. package/src/components/players/MistWebRTCPlayer/index.tsx +19 -10
  24. package/src/components/players/NativePlayer.tsx +10 -12
  25. package/src/components/players/VideoJsPlayer.tsx +13 -11
  26. package/src/context/PlayerContext.tsx +4 -8
  27. package/src/context/index.ts +3 -3
  28. package/src/hooks/useMetaTrack.ts +27 -27
  29. package/src/hooks/usePlaybackQuality.ts +3 -3
  30. package/src/hooks/usePlayerController.ts +186 -138
  31. package/src/hooks/usePlayerSelection.ts +6 -6
  32. package/src/hooks/useStreamState.ts +51 -56
  33. package/src/hooks/useTelemetry.ts +18 -3
  34. package/src/hooks/useViewerEndpoints.ts +34 -23
  35. package/src/index.tsx +36 -28
  36. package/src/types.ts +8 -8
  37. package/src/ui/badge.tsx +6 -5
  38. package/src/ui/button.tsx +9 -8
  39. package/src/ui/context-menu.tsx +42 -61
  40. package/src/ui/select.tsx +13 -7
  41. package/src/ui/slider.tsx +18 -29
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import type { StreamStatus } from '../types';
1
+ import React from "react";
2
+ import type { StreamStatus } from "../types";
3
3
 
4
4
  export interface StreamStateOverlayProps {
5
5
  /** Current stream status */
@@ -23,43 +23,93 @@ function StatusIcon({ status }: { status: StreamStatus }) {
23
23
  const iconClass = "w-5 h-5";
24
24
 
25
25
  switch (status) {
26
- case 'ONLINE':
26
+ case "ONLINE":
27
27
  return (
28
- <svg className={`${iconClass} fw-status-online`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
28
+ <svg
29
+ className={`${iconClass} fw-status-online`}
30
+ fill="none"
31
+ viewBox="0 0 24 24"
32
+ stroke="currentColor"
33
+ >
29
34
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
30
35
  </svg>
31
36
  );
32
37
 
33
- case 'OFFLINE':
38
+ case "OFFLINE":
34
39
  return (
35
- <svg className={`${iconClass} fw-status-offline`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
36
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
40
+ <svg
41
+ className={`${iconClass} fw-status-offline`}
42
+ fill="none"
43
+ viewBox="0 0 24 24"
44
+ stroke="currentColor"
45
+ >
46
+ <path
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ strokeWidth={2}
50
+ d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
51
+ />
37
52
  </svg>
38
53
  );
39
54
 
40
- case 'INITIALIZING':
41
- case 'BOOTING':
42
- case 'WAITING_FOR_DATA':
55
+ case "INITIALIZING":
56
+ case "BOOTING":
57
+ case "WAITING_FOR_DATA":
43
58
  return (
44
- <svg className={`${iconClass} fw-status-warning animate-spin`} fill="none" viewBox="0 0 24 24">
45
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
46
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
59
+ <svg
60
+ className={`${iconClass} fw-status-warning animate-spin`}
61
+ fill="none"
62
+ viewBox="0 0 24 24"
63
+ >
64
+ <circle
65
+ className="opacity-25"
66
+ cx="12"
67
+ cy="12"
68
+ r="10"
69
+ stroke="currentColor"
70
+ strokeWidth="4"
71
+ />
72
+ <path
73
+ className="opacity-75"
74
+ fill="currentColor"
75
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
76
+ />
47
77
  </svg>
48
78
  );
49
79
 
50
- case 'SHUTTING_DOWN':
80
+ case "SHUTTING_DOWN":
51
81
  return (
52
- <svg className={`${iconClass} fw-status-warning`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
53
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
82
+ <svg
83
+ className={`${iconClass} fw-status-warning`}
84
+ fill="none"
85
+ viewBox="0 0 24 24"
86
+ stroke="currentColor"
87
+ >
88
+ <path
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ strokeWidth={2}
92
+ d="M13 10V3L4 14h7v7l9-11h-7z"
93
+ />
54
94
  </svg>
55
95
  );
56
96
 
57
- case 'ERROR':
58
- case 'INVALID':
97
+ case "ERROR":
98
+ case "INVALID":
59
99
  default:
60
100
  return (
61
- <svg className={`${iconClass} fw-status-offline`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
62
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
101
+ <svg
102
+ className={`${iconClass} fw-status-offline`}
103
+ fill="none"
104
+ viewBox="0 0 24 24"
105
+ stroke="currentColor"
106
+ >
107
+ <path
108
+ strokeLinecap="round"
109
+ strokeLinejoin="round"
110
+ strokeWidth={2}
111
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
112
+ />
63
113
  </svg>
64
114
  );
65
115
  }
@@ -70,15 +120,24 @@ function StatusIcon({ status }: { status: StreamStatus }) {
70
120
  */
71
121
  function getStatusLabel(status: StreamStatus): string {
72
122
  switch (status) {
73
- case 'ONLINE': return 'ONLINE';
74
- case 'OFFLINE': return 'OFFLINE';
75
- case 'INITIALIZING': return 'INITIALIZING';
76
- case 'BOOTING': return 'STARTING';
77
- case 'WAITING_FOR_DATA': return 'WAITING';
78
- case 'SHUTTING_DOWN': return 'ENDING';
79
- case 'ERROR': return 'ERROR';
80
- case 'INVALID': return 'INVALID';
81
- default: return 'STATUS';
123
+ case "ONLINE":
124
+ return "ONLINE";
125
+ case "OFFLINE":
126
+ return "OFFLINE";
127
+ case "INITIALIZING":
128
+ return "INITIALIZING";
129
+ case "BOOTING":
130
+ return "STARTING";
131
+ case "WAITING_FOR_DATA":
132
+ return "WAITING";
133
+ case "SHUTTING_DOWN":
134
+ return "ENDING";
135
+ case "ERROR":
136
+ return "ERROR";
137
+ case "INVALID":
138
+ return "INVALID";
139
+ default:
140
+ return "STATUS";
82
141
  }
83
142
  }
84
143
 
@@ -94,26 +153,26 @@ export const StreamStateOverlay: React.FC<StreamStateOverlayProps> = ({
94
153
  percentage,
95
154
  onRetry,
96
155
  visible = true,
97
- className = '',
156
+ className = "",
98
157
  }) => {
99
- if (!visible || status === 'ONLINE') {
158
+ if (!visible || status === "ONLINE") {
100
159
  return null;
101
160
  }
102
161
 
103
- const showRetry = status === 'ERROR' || status === 'INVALID' || status === 'OFFLINE';
104
- const showProgress = status === 'INITIALIZING' && percentage !== undefined;
162
+ const showRetry = status === "ERROR" || status === "INVALID" || status === "OFFLINE";
163
+ const showProgress = status === "INITIALIZING" && percentage !== undefined;
105
164
 
106
165
  return (
107
166
  <div
108
167
  className={`absolute inset-0 z-20 flex items-center justify-center ${className}`}
109
- style={{ backgroundColor: 'hsl(var(--tn-bg-dark) / 0.8)', backdropFilter: 'blur(4px)' }}
168
+ style={{ backgroundColor: "hsl(var(--tn-bg-dark) / 0.8)", backdropFilter: "blur(4px)" }}
110
169
  role="status"
111
170
  aria-live="polite"
112
171
  >
113
172
  {/* Slab container - no rounded corners, seam borders */}
114
173
  <div
115
174
  className="fw-slab w-[280px] max-w-[90%]"
116
- style={{ backgroundColor: 'hsl(var(--tn-bg) / 0.95)' }}
175
+ style={{ backgroundColor: "hsl(var(--tn-bg) / 0.95)" }}
117
176
  >
118
177
  {/* Slab header - status label with icon */}
119
178
  <div className="fw-slab-header flex items-center gap-2">
@@ -123,7 +182,7 @@ export const StreamStateOverlay: React.FC<StreamStateOverlayProps> = ({
123
182
 
124
183
  {/* Slab body - message and progress */}
125
184
  <div className="fw-slab-body">
126
- <p className="text-sm" style={{ color: 'hsl(var(--tn-fg))' }}>
185
+ <p className="text-sm" style={{ color: "hsl(var(--tn-fg))" }}>
127
186
  {message}
128
187
  </p>
129
188
 
@@ -132,33 +191,30 @@ export const StreamStateOverlay: React.FC<StreamStateOverlayProps> = ({
132
191
  {/* Progress bar - no rounded corners */}
133
192
  <div
134
193
  className="h-1.5 w-full overflow-hidden"
135
- style={{ backgroundColor: 'hsl(var(--tn-bg-visual))' }}
194
+ style={{ backgroundColor: "hsl(var(--tn-bg-visual))" }}
136
195
  >
137
196
  <div
138
197
  className="h-full transition-all duration-300"
139
198
  style={{
140
199
  width: `${Math.min(100, percentage)}%`,
141
- backgroundColor: 'hsl(var(--tn-yellow))',
200
+ backgroundColor: "hsl(var(--tn-yellow))",
142
201
  }}
143
202
  />
144
203
  </div>
145
- <p
146
- className="mt-1.5 text-xs font-mono"
147
- style={{ color: 'hsl(var(--tn-fg-dark))' }}
148
- >
204
+ <p className="mt-1.5 text-xs font-mono" style={{ color: "hsl(var(--tn-fg-dark))" }}>
149
205
  {Math.round(percentage)}%
150
206
  </p>
151
207
  </div>
152
208
  )}
153
209
 
154
- {status === 'OFFLINE' && (
155
- <p className="mt-2 text-xs" style={{ color: 'hsl(var(--tn-fg-dark))' }}>
210
+ {status === "OFFLINE" && (
211
+ <p className="mt-2 text-xs" style={{ color: "hsl(var(--tn-fg-dark))" }}>
156
212
  The stream will start when the broadcaster goes live
157
213
  </p>
158
214
  )}
159
215
 
160
- {(status === 'BOOTING' || status === 'WAITING_FOR_DATA') && (
161
- <p className="mt-2 text-xs" style={{ color: 'hsl(var(--tn-fg-dark))' }}>
216
+ {(status === "BOOTING" || status === "WAITING_FOR_DATA") && (
217
+ <p className="mt-2 text-xs" style={{ color: "hsl(var(--tn-fg-dark))" }}>
162
218
  Please wait while the stream prepares...
163
219
  </p>
164
220
  )}
@@ -167,11 +223,11 @@ export const StreamStateOverlay: React.FC<StreamStateOverlayProps> = ({
167
223
  {!showRetry && (
168
224
  <div
169
225
  className="mt-3 flex items-center gap-2 text-xs"
170
- style={{ color: 'hsl(var(--tn-fg-dark))' }}
226
+ style={{ color: "hsl(var(--tn-fg-dark))" }}
171
227
  >
172
228
  <span
173
229
  className="h-1.5 w-1.5 animate-pulse"
174
- style={{ backgroundColor: 'hsl(var(--tn-cyan))' }}
230
+ style={{ backgroundColor: "hsl(var(--tn-cyan))" }}
175
231
  />
176
232
  <span>Checking stream status...</span>
177
233
  </div>
@@ -185,7 +241,7 @@ export const StreamStateOverlay: React.FC<StreamStateOverlayProps> = ({
185
241
  type="button"
186
242
  onClick={onRetry}
187
243
  className="fw-btn-flush py-2.5 text-xs font-medium uppercase tracking-wide"
188
- style={{ color: 'hsl(var(--tn-blue))' }}
244
+ style={{ color: "hsl(var(--tn-blue))" }}
189
245
  aria-label="Retry connection"
190
246
  >
191
247
  Retry Connection
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import type { SubtitleCue, MetaTrackEvent } from '../types';
1
+ import React, { useEffect, useState, useRef } from "react";
2
+ import type { SubtitleCue, MetaTrackEvent } from "../types";
3
3
 
4
4
  export interface SubtitleRendererProps {
5
5
  /** Current video playback time in seconds */
@@ -40,48 +40,48 @@ export interface SubtitleStyle {
40
40
  }
41
41
 
42
42
  const DEFAULT_STYLE: SubtitleStyle = {
43
- fontSize: '1.5rem',
44
- fontFamily: 'system-ui, -apple-system, sans-serif',
45
- color: 'white',
46
- backgroundColor: 'rgba(0, 0, 0, 0.75)',
47
- textShadow: '2px 2px 4px rgba(0, 0, 0, 0.5)',
48
- bottom: '5%',
49
- maxWidth: '90%',
50
- padding: '0.5em 1em',
51
- borderRadius: '4px',
43
+ fontSize: "1.5rem",
44
+ fontFamily: "system-ui, -apple-system, sans-serif",
45
+ color: "white",
46
+ backgroundColor: "rgba(0, 0, 0, 0.75)",
47
+ textShadow: "2px 2px 4px rgba(0, 0, 0, 0.5)",
48
+ bottom: "5%",
49
+ maxWidth: "90%",
50
+ padding: "0.5em 1em",
51
+ borderRadius: "4px",
52
52
  };
53
53
 
54
54
  /**
55
55
  * Parse subtitle cue from meta track event data
56
56
  */
57
57
  function parseSubtitleCue(data: unknown): SubtitleCue | null {
58
- if (typeof data !== 'object' || data === null) return null;
58
+ if (typeof data !== "object" || data === null) return null;
59
59
 
60
60
  const obj = data as Record<string, unknown>;
61
61
 
62
62
  // Extract text
63
- const text = typeof obj.text === 'string' ? obj.text : String(obj.text ?? '');
63
+ const text = typeof obj.text === "string" ? obj.text : String(obj.text ?? "");
64
64
  if (!text) return null;
65
65
 
66
66
  // Extract timing
67
67
  let startTime = 0;
68
68
  let endTime = Infinity;
69
69
 
70
- if ('startTime' in obj) startTime = Number(obj.startTime);
71
- else if ('start' in obj) startTime = Number(obj.start);
70
+ if ("startTime" in obj) startTime = Number(obj.startTime);
71
+ else if ("start" in obj) startTime = Number(obj.start);
72
72
 
73
- if ('endTime' in obj) endTime = Number(obj.endTime);
74
- else if ('end' in obj) endTime = Number(obj.end);
73
+ if ("endTime" in obj) endTime = Number(obj.endTime);
74
+ else if ("end" in obj) endTime = Number(obj.end);
75
75
 
76
76
  // Extract ID
77
- const id = typeof obj.id === 'string' ? obj.id : String(Date.now());
77
+ const id = typeof obj.id === "string" ? obj.id : String(Date.now());
78
78
 
79
79
  return {
80
80
  id,
81
81
  text,
82
82
  startTime,
83
83
  endTime,
84
- lang: typeof obj.lang === 'string' ? obj.lang : undefined,
84
+ lang: typeof obj.lang === "string" ? obj.lang : undefined,
85
85
  };
86
86
  }
87
87
 
@@ -121,10 +121,10 @@ export const SubtitleRenderer: React.FC<SubtitleRendererProps> = ({
121
121
  subscribeToMetaTrack,
122
122
  metaTrackId,
123
123
  style: customStyle,
124
- className = '',
124
+ className = "",
125
125
  }) => {
126
126
  const [liveCues, setLiveCues] = useState<SubtitleCue[]>([]);
127
- const [displayedText, setDisplayedText] = useState<string>('');
127
+ const [displayedText, setDisplayedText] = useState<string>("");
128
128
  const lastCueIdRef = useRef<string | null>(null);
129
129
 
130
130
  const style = { ...DEFAULT_STYLE, ...customStyle };
@@ -139,12 +139,12 @@ export const SubtitleRenderer: React.FC<SubtitleRendererProps> = ({
139
139
  }
140
140
 
141
141
  const handleMetaEvent = (event: MetaTrackEvent) => {
142
- if (event.type === 'subtitle') {
142
+ if (event.type === "subtitle") {
143
143
  const cue = parseSubtitleCue(event.data);
144
144
  if (cue) {
145
- setLiveCues(prev => {
145
+ setLiveCues((prev) => {
146
146
  // Deduplicate by ID
147
- const existing = prev.find(c => c.id === cue.id);
147
+ const existing = prev.find((c) => c.id === cue.id);
148
148
  if (existing) return prev;
149
149
 
150
150
  // Keep last 50 cues max
@@ -165,13 +165,13 @@ export const SubtitleRenderer: React.FC<SubtitleRendererProps> = ({
165
165
  // Find active cue based on current time
166
166
  useEffect(() => {
167
167
  if (!enabled) {
168
- setDisplayedText('');
168
+ setDisplayedText("");
169
169
  return;
170
170
  }
171
171
 
172
172
  // Find cue that matches current time
173
173
  const currentTimeMs = currentTime * 1000; // Convert to ms if needed
174
- const activeCue = allCues.find(cue => {
174
+ const activeCue = allCues.find((cue) => {
175
175
  const start = cue.startTime;
176
176
  const end = cue.endTime;
177
177
  return currentTimeMs >= start && currentTimeMs < end;
@@ -181,7 +181,7 @@ export const SubtitleRenderer: React.FC<SubtitleRendererProps> = ({
181
181
  setDisplayedText(activeCue.text);
182
182
  lastCueIdRef.current = activeCue.id;
183
183
  } else {
184
- setDisplayedText('');
184
+ setDisplayedText("");
185
185
  lastCueIdRef.current = null;
186
186
  }
187
187
  }, [enabled, currentTime, allCues]);
@@ -190,9 +190,9 @@ export const SubtitleRenderer: React.FC<SubtitleRendererProps> = ({
190
190
  useEffect(() => {
191
191
  const currentTimeMs = currentTime * 1000;
192
192
 
193
- setLiveCues(prev => {
193
+ setLiveCues((prev) => {
194
194
  // Remove cues that are more than 30 seconds old
195
- return prev.filter(cue => {
195
+ return prev.filter((cue) => {
196
196
  const endTime = cue.endTime === Infinity ? cue.startTime + 10000 : cue.endTime;
197
197
  return endTime >= currentTimeMs - 30000;
198
198
  });
@@ -9,7 +9,7 @@ const ThumbnailOverlay: React.FC<ThumbnailOverlayProps> = ({
9
9
  message,
10
10
  showUnmuteMessage = false,
11
11
  style,
12
- className
12
+ className,
13
13
  }) => {
14
14
  const handleClick = (e: React.MouseEvent | React.KeyboardEvent) => {
15
15
  e.stopPropagation();
@@ -29,8 +29,9 @@ const ThumbnailOverlay: React.FC<ThumbnailOverlayProps> = ({
29
29
  }}
30
30
  style={style}
31
31
  className={cn(
32
- "fw-player-thumbnail relative flex h-full min-h-[280px] w-full cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-slate-950 text-foreground outline-none transition focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
33
- , className)}
32
+ "fw-player-thumbnail relative flex h-full min-h-[280px] w-full cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-slate-950 text-foreground outline-none transition focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
33
+ className
34
+ )}
34
35
  >
35
36
  {thumbnailUrl && (
36
37
  <div
@@ -73,9 +74,7 @@ const ThumbnailOverlay: React.FC<ThumbnailOverlayProps> = ({
73
74
  </svg>
74
75
  </Button>
75
76
  <div className="w-full rounded-lg border border-white/10 bg-black/70 p-5 text-white shadow-inner backdrop-blur">
76
- <p className="text-base font-semibold text-primary">
77
- {message ?? "Click to play"}
78
- </p>
77
+ <p className="text-base font-semibold text-primary">{message ?? "Click to play"}</p>
79
78
  <p className="mt-1 text-xs text-white/70">
80
79
  {message ? "Start streaming instantly" : "Jump into the live feed"}
81
80
  </p>
@@ -31,15 +31,9 @@ const TitleOverlay: React.FC<TitleOverlayProps> = ({
31
31
  className
32
32
  )}
33
33
  >
34
- {title && (
35
- <h2 className="text-white text-sm font-medium truncate max-w-[80%]">
36
- {title}
37
- </h2>
38
- )}
34
+ {title && <h2 className="text-white text-sm font-medium truncate max-w-[80%]">{title}</h2>}
39
35
  {description && (
40
- <p className="text-white/70 text-xs mt-0.5 line-clamp-2 max-w-[70%]">
41
- {description}
42
- </p>
36
+ <p className="text-white/70 text-xs mt-0.5 line-clamp-2 max-w-[70%]">{description}</p>
43
37
  )}
44
38
  </div>
45
39
  );
@@ -5,8 +5,8 @@
5
5
  * The implementation is in @livepeer-frameworks/player-core.
6
6
  */
7
7
 
8
- import React, { useEffect, useRef } from 'react';
9
- import { DashJsPlayerImpl } from '@livepeer-frameworks/player-core';
8
+ import React, { useEffect, useRef } from "react";
9
+ import { DashJsPlayerImpl } from "@livepeer-frameworks/player-core";
10
10
 
11
11
  // Re-export the implementation from core for backwards compatibility
12
12
  export { DashJsPlayerImpl };
@@ -25,7 +25,7 @@ const DashJsPlayer: React.FC<Props> = ({
25
25
  muted = true,
26
26
  autoPlay = true,
27
27
  controls = true,
28
- onError
28
+ onError,
29
29
  }) => {
30
30
  const containerRef = useRef<HTMLDivElement>(null);
31
31
  const playerRef = useRef<DashJsPlayerImpl | null>(null);
@@ -36,13 +36,15 @@ const DashJsPlayer: React.FC<Props> = ({
36
36
  const player = new DashJsPlayerImpl();
37
37
  playerRef.current = player;
38
38
 
39
- player.initialize(
40
- containerRef.current,
41
- { url: src, type: 'dash/video/mp4' },
42
- { autoplay: autoPlay, muted, controls }
43
- ).catch((e) => {
44
- onError?.(e instanceof Error ? e : new Error(String(e)));
45
- });
39
+ player
40
+ .initialize(
41
+ containerRef.current,
42
+ { url: src, type: "dash/video/mp4" },
43
+ { autoplay: autoPlay, muted, controls }
44
+ )
45
+ .catch((e) => {
46
+ onError?.(e instanceof Error ? e : new Error(String(e)));
47
+ });
46
48
 
47
49
  return () => {
48
50
  player.destroy();
@@ -50,7 +52,7 @@ const DashJsPlayer: React.FC<Props> = ({
50
52
  };
51
53
  }, [src, muted, autoPlay, controls, onError]);
52
54
 
53
- return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
55
+ return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
54
56
  };
55
57
 
56
58
  export default DashJsPlayer;
@@ -5,8 +5,8 @@
5
5
  * The implementation is in @livepeer-frameworks/player-core.
6
6
  */
7
7
 
8
- import React, { useEffect, useRef } from 'react';
9
- import { HlsJsPlayerImpl } from '@livepeer-frameworks/player-core';
8
+ import React, { useEffect, useRef } from "react";
9
+ import { HlsJsPlayerImpl } from "@livepeer-frameworks/player-core";
10
10
 
11
11
  // Re-export the implementation from core for backwards compatibility
12
12
  export { HlsJsPlayerImpl };
@@ -25,7 +25,7 @@ const HlsJsPlayer: React.FC<Props> = ({
25
25
  muted = true,
26
26
  autoPlay = true,
27
27
  controls = true,
28
- onError
28
+ onError,
29
29
  }) => {
30
30
  const containerRef = useRef<HTMLDivElement>(null);
31
31
  const playerRef = useRef<HlsJsPlayerImpl | null>(null);
@@ -36,13 +36,15 @@ const HlsJsPlayer: React.FC<Props> = ({
36
36
  const player = new HlsJsPlayerImpl();
37
37
  playerRef.current = player;
38
38
 
39
- player.initialize(
40
- containerRef.current,
41
- { url: src, type: 'html5/application/vnd.apple.mpegurl' },
42
- { autoplay: autoPlay, muted, controls }
43
- ).catch((e) => {
44
- onError?.(e instanceof Error ? e : new Error(String(e)));
45
- });
39
+ player
40
+ .initialize(
41
+ containerRef.current,
42
+ { url: src, type: "html5/application/vnd.apple.mpegurl" },
43
+ { autoplay: autoPlay, muted, controls }
44
+ )
45
+ .catch((e) => {
46
+ onError?.(e instanceof Error ? e : new Error(String(e)));
47
+ });
46
48
 
47
49
  return () => {
48
50
  player.destroy();
@@ -50,7 +52,7 @@ const HlsJsPlayer: React.FC<Props> = ({
50
52
  };
51
53
  }, [src, muted, autoPlay, controls, onError]);
52
54
 
53
- return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
55
+ return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
54
56
  };
55
57
 
56
58
  export default HlsJsPlayer;
@@ -5,8 +5,8 @@
5
5
  * The implementation is in @livepeer-frameworks/player-core.
6
6
  */
7
7
 
8
- import React, { useEffect, useRef } from 'react';
9
- import { MewsWsPlayerImpl } from '@livepeer-frameworks/player-core';
8
+ import React, { useEffect, useRef } from "react";
9
+ import { MewsWsPlayerImpl } from "@livepeer-frameworks/player-core";
10
10
 
11
11
  // Re-export the implementation from core for backwards compatibility
12
12
  export { MewsWsPlayerImpl };
@@ -25,7 +25,7 @@ const MewsWsPlayer: React.FC<Props> = ({
25
25
  muted = true,
26
26
  autoPlay = true,
27
27
  controls = true,
28
- onError
28
+ onError,
29
29
  }) => {
30
30
  const containerRef = useRef<HTMLDivElement>(null);
31
31
  const playerRef = useRef<MewsWsPlayerImpl | null>(null);
@@ -36,13 +36,15 @@ const MewsWsPlayer: React.FC<Props> = ({
36
36
  const player = new MewsWsPlayerImpl();
37
37
  playerRef.current = player;
38
38
 
39
- player.initialize(
40
- containerRef.current,
41
- { url: wsUrl, type: 'ws/video/mp4' },
42
- { autoplay: autoPlay, muted, controls }
43
- ).catch((e) => {
44
- onError?.(e instanceof Error ? e : new Error(String(e)));
45
- });
39
+ player
40
+ .initialize(
41
+ containerRef.current,
42
+ { url: wsUrl, type: "ws/video/mp4" },
43
+ { autoplay: autoPlay, muted, controls }
44
+ )
45
+ .catch((e) => {
46
+ onError?.(e instanceof Error ? e : new Error(String(e)));
47
+ });
46
48
 
47
49
  return () => {
48
50
  player.destroy();
@@ -50,7 +52,7 @@ const MewsWsPlayer: React.FC<Props> = ({
50
52
  };
51
53
  }, [wsUrl, muted, autoPlay, controls, onError]);
52
54
 
53
- return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
55
+ return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
54
56
  };
55
57
 
56
58
  export default MewsWsPlayer;
@@ -5,8 +5,8 @@
5
5
  * The implementation is in @livepeer-frameworks/player-core.
6
6
  */
7
7
 
8
- import React, { useEffect, useRef } from 'react';
9
- import { MistPlayerImpl } from '@livepeer-frameworks/player-core';
8
+ import React, { useEffect, useRef } from "react";
9
+ import { MistPlayerImpl } from "@livepeer-frameworks/player-core";
10
10
 
11
11
  // Re-export the implementation from core for backwards compatibility
12
12
  export { MistPlayerImpl };
@@ -29,7 +29,7 @@ const MistPlayer: React.FC<Props> = ({
29
29
  autoPlay = true,
30
30
  controls = true,
31
31
  devMode = false,
32
- onError
32
+ onError,
33
33
  }) => {
34
34
  const containerRef = useRef<HTMLDivElement>(null);
35
35
  const playerRef = useRef<MistPlayerImpl | null>(null);
@@ -40,13 +40,15 @@ const MistPlayer: React.FC<Props> = ({
40
40
  const player = new MistPlayerImpl();
41
41
  playerRef.current = player;
42
42
 
43
- player.initialize(
44
- containerRef.current,
45
- { url: src, type: 'mist/legacy', streamName },
46
- { autoplay: autoPlay, muted, controls, devMode }
47
- ).catch((e) => {
48
- onError?.(e instanceof Error ? e : new Error(String(e)));
49
- });
43
+ player
44
+ .initialize(
45
+ containerRef.current,
46
+ { url: src, type: "mist/legacy", streamName },
47
+ { autoplay: autoPlay, muted, controls, devMode }
48
+ )
49
+ .catch((e) => {
50
+ onError?.(e instanceof Error ? e : new Error(String(e)));
51
+ });
50
52
 
51
53
  return () => {
52
54
  player.destroy();
@@ -54,7 +56,7 @@ const MistPlayer: React.FC<Props> = ({
54
56
  };
55
57
  }, [src, streamName, muted, autoPlay, controls, devMode, onError]);
56
58
 
57
- return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
59
+ return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
58
60
  };
59
61
 
60
62
  export default MistPlayer;