@livepeer-frameworks/player-react 0.0.4 → 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.
- package/README.md +16 -5
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/types/components/PlayerControls.d.ts +2 -0
- package/dist/types/components/StatsPanel.d.ts +2 -14
- package/dist/types/hooks/useMetaTrack.d.ts +1 -1
- package/dist/types/hooks/usePlayerController.d.ts +2 -0
- package/dist/types/hooks/useStreamState.d.ts +1 -1
- package/dist/types/hooks/useTelemetry.d.ts +1 -1
- package/dist/types/hooks/useViewerEndpoints.d.ts +2 -2
- package/dist/types/types.d.ts +1 -1
- package/dist/types/ui/button.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/DevModePanel.tsx +249 -170
- package/src/components/Icons.tsx +105 -25
- package/src/components/IdleScreen.tsx +262 -142
- package/src/components/LoadingScreen.tsx +171 -153
- package/src/components/LogoOverlay.tsx +3 -6
- package/src/components/Player.tsx +86 -74
- package/src/components/PlayerControls.tsx +351 -263
- package/src/components/PlayerErrorBoundary.tsx +6 -13
- package/src/components/SeekBar.tsx +96 -88
- package/src/components/SkipIndicator.tsx +2 -12
- package/src/components/SpeedIndicator.tsx +2 -11
- package/src/components/StatsPanel.tsx +65 -34
- package/src/components/StreamStateOverlay.tsx +105 -49
- package/src/components/SubtitleRenderer.tsx +29 -29
- package/src/components/ThumbnailOverlay.tsx +5 -6
- package/src/components/TitleOverlay.tsx +2 -8
- package/src/components/players/DashJsPlayer.tsx +13 -11
- package/src/components/players/HlsJsPlayer.tsx +13 -11
- package/src/components/players/MewsWsPlayer/index.tsx +13 -11
- package/src/components/players/MistPlayer.tsx +13 -11
- package/src/components/players/MistWebRTCPlayer/index.tsx +19 -10
- package/src/components/players/NativePlayer.tsx +10 -12
- package/src/components/players/VideoJsPlayer.tsx +13 -11
- package/src/context/PlayerContext.tsx +4 -8
- package/src/context/index.ts +3 -3
- package/src/hooks/useMetaTrack.ts +28 -28
- package/src/hooks/usePlaybackQuality.ts +3 -3
- package/src/hooks/usePlayerController.ts +186 -140
- package/src/hooks/usePlayerSelection.ts +6 -6
- package/src/hooks/useStreamState.ts +53 -58
- package/src/hooks/useTelemetry.ts +19 -4
- package/src/hooks/useViewerEndpoints.ts +40 -30
- package/src/index.tsx +36 -28
- package/src/types.ts +9 -9
- package/src/ui/badge.tsx +6 -5
- package/src/ui/button.tsx +9 -8
- package/src/ui/context-menu.tsx +42 -61
- package/src/ui/select.tsx +13 -7
- package/src/ui/slider.tsx +18 -29
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { useEffect, useState, useRef, useCallback } from
|
|
2
|
-
import type {
|
|
3
|
-
UseStreamStateOptions,
|
|
4
|
-
StreamState,
|
|
5
|
-
StreamStatus,
|
|
6
|
-
MistStreamInfo,
|
|
7
|
-
} from '../types';
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
2
|
+
import type { UseStreamStateOptions, StreamState, StreamStatus, MistStreamInfo } from "../types";
|
|
8
3
|
|
|
9
4
|
/**
|
|
10
5
|
* Parse MistServer error string into StreamStatus enum
|
|
@@ -12,14 +7,14 @@ import type {
|
|
|
12
7
|
function parseErrorToStatus(error: string): StreamStatus {
|
|
13
8
|
const lowerError = error.toLowerCase();
|
|
14
9
|
|
|
15
|
-
if (lowerError.includes(
|
|
16
|
-
if (lowerError.includes(
|
|
17
|
-
if (lowerError.includes(
|
|
18
|
-
if (lowerError.includes(
|
|
19
|
-
if (lowerError.includes(
|
|
20
|
-
if (lowerError.includes(
|
|
10
|
+
if (lowerError.includes("offline")) return "OFFLINE";
|
|
11
|
+
if (lowerError.includes("initializing")) return "INITIALIZING";
|
|
12
|
+
if (lowerError.includes("booting")) return "BOOTING";
|
|
13
|
+
if (lowerError.includes("waiting for data")) return "WAITING_FOR_DATA";
|
|
14
|
+
if (lowerError.includes("shutting down")) return "SHUTTING_DOWN";
|
|
15
|
+
if (lowerError.includes("invalid")) return "INVALID";
|
|
21
16
|
|
|
22
|
-
return
|
|
17
|
+
return "ERROR";
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
/**
|
|
@@ -27,25 +22,25 @@ function parseErrorToStatus(error: string): StreamStatus {
|
|
|
27
22
|
*/
|
|
28
23
|
function getStatusMessage(status: StreamStatus, percentage?: number): string {
|
|
29
24
|
switch (status) {
|
|
30
|
-
case
|
|
31
|
-
return
|
|
32
|
-
case
|
|
33
|
-
return
|
|
34
|
-
case
|
|
25
|
+
case "ONLINE":
|
|
26
|
+
return "Stream is online";
|
|
27
|
+
case "OFFLINE":
|
|
28
|
+
return "Stream is offline";
|
|
29
|
+
case "INITIALIZING":
|
|
35
30
|
return percentage !== undefined
|
|
36
31
|
? `Initializing... ${Math.round(percentage * 10) / 10}%`
|
|
37
|
-
:
|
|
38
|
-
case
|
|
39
|
-
return
|
|
40
|
-
case
|
|
41
|
-
return
|
|
42
|
-
case
|
|
43
|
-
return
|
|
44
|
-
case
|
|
45
|
-
return
|
|
46
|
-
case
|
|
32
|
+
: "Stream is initializing";
|
|
33
|
+
case "BOOTING":
|
|
34
|
+
return "Stream is starting up";
|
|
35
|
+
case "WAITING_FOR_DATA":
|
|
36
|
+
return "Waiting for stream data";
|
|
37
|
+
case "SHUTTING_DOWN":
|
|
38
|
+
return "Stream is shutting down";
|
|
39
|
+
case "INVALID":
|
|
40
|
+
return "Stream status is invalid";
|
|
41
|
+
case "ERROR":
|
|
47
42
|
default:
|
|
48
|
-
return
|
|
43
|
+
return "Stream error";
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
46
|
|
|
@@ -53,9 +48,9 @@ function getStatusMessage(status: StreamStatus, percentage?: number): string {
|
|
|
53
48
|
* Initial stream state
|
|
54
49
|
*/
|
|
55
50
|
const initialState: StreamState = {
|
|
56
|
-
status:
|
|
51
|
+
status: "OFFLINE",
|
|
57
52
|
isOnline: false,
|
|
58
|
-
message:
|
|
53
|
+
message: "Connecting...",
|
|
59
54
|
lastUpdate: 0,
|
|
60
55
|
};
|
|
61
56
|
|
|
@@ -70,7 +65,7 @@ const initialState: StreamState = {
|
|
|
70
65
|
* ```tsx
|
|
71
66
|
* const { status, isOnline, message } = useStreamState({
|
|
72
67
|
* mistBaseUrl: 'https://mist.example.com',
|
|
73
|
-
* streamName: '
|
|
68
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
74
69
|
* pollInterval: 3000,
|
|
75
70
|
* });
|
|
76
71
|
* ```
|
|
@@ -115,7 +110,7 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
115
110
|
const status = parseErrorToStatus(data.error);
|
|
116
111
|
const message = data.on_error || getStatusMessage(status, data.perc);
|
|
117
112
|
|
|
118
|
-
setState(prev => ({
|
|
113
|
+
setState((prev) => ({
|
|
119
114
|
status,
|
|
120
115
|
isOnline: false,
|
|
121
116
|
message,
|
|
@@ -128,10 +123,10 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
128
123
|
// Stream is online with valid metadata
|
|
129
124
|
// Merge new data with existing streamInfo to preserve source/tracks from initial fetch
|
|
130
125
|
// WebSocket updates may not include source array - only status updates
|
|
131
|
-
setState(prev => {
|
|
126
|
+
setState((prev) => {
|
|
132
127
|
const mergedStreamInfo: MistStreamInfo = {
|
|
133
|
-
...prev.streamInfo,
|
|
134
|
-
...data,
|
|
128
|
+
...prev.streamInfo, // Keep existing source/meta if present
|
|
129
|
+
...data, // Override with new data
|
|
135
130
|
// Explicitly preserve source if not in new data
|
|
136
131
|
source: data.source || prev.streamInfo?.source,
|
|
137
132
|
// Merge meta to preserve tracks
|
|
@@ -144,9 +139,9 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
144
139
|
};
|
|
145
140
|
|
|
146
141
|
return {
|
|
147
|
-
status:
|
|
142
|
+
status: "ONLINE",
|
|
148
143
|
isOnline: true,
|
|
149
|
-
message:
|
|
144
|
+
message: "Stream is online",
|
|
150
145
|
lastUpdate: Date.now(),
|
|
151
146
|
streamInfo: mergedStreamInfo,
|
|
152
147
|
};
|
|
@@ -163,11 +158,11 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
163
158
|
|
|
164
159
|
try {
|
|
165
160
|
// Build URL with MistPlayer-style params
|
|
166
|
-
const baseUrl = `${mistBaseUrl.replace(/\/$/,
|
|
161
|
+
const baseUrl = `${mistBaseUrl.replace(/\/$/, "")}/json_${encodeURIComponent(streamName)}.js`;
|
|
167
162
|
const url = `${baseUrl}?metaeverywhere=1&inclzero=1`;
|
|
168
163
|
const response = await fetch(url, {
|
|
169
|
-
method:
|
|
170
|
-
headers: {
|
|
164
|
+
method: "GET",
|
|
165
|
+
headers: { Accept: "application/json" },
|
|
171
166
|
});
|
|
172
167
|
|
|
173
168
|
if (!response.ok) {
|
|
@@ -187,13 +182,13 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
187
182
|
} catch (error) {
|
|
188
183
|
if (!mountedRef.current) return;
|
|
189
184
|
|
|
190
|
-
setState(prev => ({
|
|
185
|
+
setState((prev) => ({
|
|
191
186
|
...prev,
|
|
192
|
-
status:
|
|
187
|
+
status: "ERROR",
|
|
193
188
|
isOnline: false,
|
|
194
|
-
message: error instanceof Error ? error.message :
|
|
189
|
+
message: error instanceof Error ? error.message : "Connection failed",
|
|
195
190
|
lastUpdate: Date.now(),
|
|
196
|
-
error: error instanceof Error ? error.message :
|
|
191
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
197
192
|
}));
|
|
198
193
|
}
|
|
199
194
|
|
|
@@ -222,9 +217,9 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
222
217
|
try {
|
|
223
218
|
// Convert http(s) to ws(s)
|
|
224
219
|
const wsUrl = mistBaseUrl
|
|
225
|
-
.replace(/^http:/,
|
|
226
|
-
.replace(/^https:/,
|
|
227
|
-
.replace(/\/$/,
|
|
220
|
+
.replace(/^http:/, "ws:")
|
|
221
|
+
.replace(/^https:/, "wss:")
|
|
222
|
+
.replace(/\/$/, "");
|
|
228
223
|
|
|
229
224
|
// Build URL with MistPlayer-style params
|
|
230
225
|
const url = `${wsUrl}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`;
|
|
@@ -235,7 +230,7 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
235
230
|
wsTimeoutRef.current = setTimeout(() => {
|
|
236
231
|
if (ws.readyState <= WebSocket.OPEN) {
|
|
237
232
|
if (debug) {
|
|
238
|
-
console.debug(
|
|
233
|
+
console.debug("[useStreamState] WebSocket timeout (5s), falling back to HTTP polling");
|
|
239
234
|
}
|
|
240
235
|
ws.close();
|
|
241
236
|
pollHttp();
|
|
@@ -244,7 +239,7 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
244
239
|
|
|
245
240
|
ws.onopen = () => {
|
|
246
241
|
if (debug) {
|
|
247
|
-
console.debug(
|
|
242
|
+
console.debug("[useStreamState] WebSocket connected");
|
|
248
243
|
}
|
|
249
244
|
setSocketReady(true);
|
|
250
245
|
};
|
|
@@ -260,12 +255,12 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
260
255
|
const data = JSON.parse(event.data) as MistStreamInfo;
|
|
261
256
|
processStreamInfo(data);
|
|
262
257
|
} catch (e) {
|
|
263
|
-
console.warn(
|
|
258
|
+
console.warn("[useStreamState] Failed to parse WebSocket message:", e);
|
|
264
259
|
}
|
|
265
260
|
};
|
|
266
261
|
|
|
267
|
-
ws.onerror = (
|
|
268
|
-
console.warn(
|
|
262
|
+
ws.onerror = (_event) => {
|
|
263
|
+
console.warn("[useStreamState] WebSocket error, falling back to HTTP polling");
|
|
269
264
|
if (wsTimeoutRef.current) {
|
|
270
265
|
clearTimeout(wsTimeoutRef.current);
|
|
271
266
|
wsTimeoutRef.current = null;
|
|
@@ -281,12 +276,12 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
281
276
|
|
|
282
277
|
// Fallback to HTTP polling or reconnect
|
|
283
278
|
if (debug) {
|
|
284
|
-
console.debug(
|
|
279
|
+
console.debug("[useStreamState] WebSocket closed, starting HTTP polling");
|
|
285
280
|
}
|
|
286
281
|
pollHttp();
|
|
287
282
|
};
|
|
288
283
|
} catch (error) {
|
|
289
|
-
console.warn(
|
|
284
|
+
console.warn("[useStreamState] WebSocket connection failed:", error);
|
|
290
285
|
// Fallback to HTTP polling
|
|
291
286
|
pollHttp();
|
|
292
287
|
}
|
|
@@ -320,7 +315,7 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
320
315
|
// Reset state when stream changes
|
|
321
316
|
setState({
|
|
322
317
|
...initialState,
|
|
323
|
-
message:
|
|
318
|
+
message: "Connecting...",
|
|
324
319
|
lastUpdate: Date.now(),
|
|
325
320
|
});
|
|
326
321
|
|
|
@@ -342,7 +337,7 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
342
337
|
// Set mounted=false FIRST before any other cleanup
|
|
343
338
|
mountedRef.current = false;
|
|
344
339
|
if (debug) {
|
|
345
|
-
console.debug(
|
|
340
|
+
console.debug("[useStreamState] cleanup starting, mountedRef set to false");
|
|
346
341
|
}
|
|
347
342
|
|
|
348
343
|
// Cleanup WebSocket timeout
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { useEffect, useRef, useCallback } from
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
TelemetryReporter,
|
|
4
|
+
type TelemetryOptions,
|
|
5
|
+
type PlaybackQuality,
|
|
6
|
+
type ContentType,
|
|
7
|
+
} from "@livepeer-frameworks/player-core";
|
|
3
8
|
|
|
4
9
|
export interface UseTelemetryOptions extends TelemetryOptions {
|
|
5
10
|
/** Video element to monitor */
|
|
@@ -36,7 +41,7 @@ export interface UseTelemetryOptions extends TelemetryOptions {
|
|
|
36
41
|
* endpoint: '/api/telemetry',
|
|
37
42
|
* interval: 5000,
|
|
38
43
|
* videoElement,
|
|
39
|
-
* contentId: '
|
|
44
|
+
* contentId: 'pk_...', // playbackId (view key)
|
|
40
45
|
* contentType: 'live',
|
|
41
46
|
* playerType: 'hlsjs',
|
|
42
47
|
* protocol: 'HLS',
|
|
@@ -87,7 +92,17 @@ export function useTelemetry(options: UseTelemetryOptions) {
|
|
|
87
92
|
reporterRef.current?.stop();
|
|
88
93
|
reporterRef.current = null;
|
|
89
94
|
};
|
|
90
|
-
}, [
|
|
95
|
+
}, [
|
|
96
|
+
enabled,
|
|
97
|
+
endpoint,
|
|
98
|
+
authToken,
|
|
99
|
+
interval,
|
|
100
|
+
batchSize,
|
|
101
|
+
contentId,
|
|
102
|
+
contentType,
|
|
103
|
+
playerType,
|
|
104
|
+
protocol,
|
|
105
|
+
]);
|
|
91
106
|
|
|
92
107
|
// Start/stop reporting when video element changes
|
|
93
108
|
useEffect(() => {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from
|
|
2
|
-
import type { ContentType } from
|
|
3
|
-
import type { ContentEndpoints } from
|
|
1
|
+
import { useEffect, useState, useRef } from "react";
|
|
2
|
+
import type { ContentType } from "@livepeer-frameworks/player-core";
|
|
3
|
+
import type { ContentEndpoints } from "../types";
|
|
4
4
|
|
|
5
5
|
const MAX_RETRIES = 3;
|
|
6
6
|
const INITIAL_DELAY_MS = 500;
|
|
7
7
|
|
|
8
8
|
interface Params {
|
|
9
9
|
gatewayUrl: string;
|
|
10
|
-
contentType: ContentType;
|
|
11
10
|
contentId: string;
|
|
11
|
+
contentType?: ContentType;
|
|
12
12
|
authToken?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ async function fetchWithRetry(
|
|
|
25
25
|
const response = await fetch(url, options);
|
|
26
26
|
return response;
|
|
27
27
|
} catch (e) {
|
|
28
|
-
lastError = e instanceof Error ? e : new Error(
|
|
28
|
+
lastError = e instanceof Error ? e : new Error("Fetch failed");
|
|
29
29
|
|
|
30
30
|
// Don't retry on abort
|
|
31
31
|
if (options.signal?.aborted) {
|
|
@@ -35,24 +35,31 @@ async function fetchWithRetry(
|
|
|
35
35
|
// Wait before retrying (exponential backoff)
|
|
36
36
|
if (attempt < maxRetries - 1) {
|
|
37
37
|
const delay = initialDelay * Math.pow(2, attempt);
|
|
38
|
-
console.warn(
|
|
39
|
-
|
|
38
|
+
console.warn(
|
|
39
|
+
`[useViewerEndpoints] Retry ${attempt + 1}/${maxRetries - 1} after ${delay}ms`
|
|
40
|
+
);
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
throw lastError ?? new Error(
|
|
46
|
+
throw lastError ?? new Error("Gateway unreachable after retries");
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export function useViewerEndpoints({
|
|
49
|
+
export function useViewerEndpoints({
|
|
50
|
+
gatewayUrl,
|
|
51
|
+
contentType: _contentType,
|
|
52
|
+
contentId,
|
|
53
|
+
authToken,
|
|
54
|
+
}: Params) {
|
|
48
55
|
const [endpoints, setEndpoints] = useState<ContentEndpoints | null>(null);
|
|
49
|
-
const [status, setStatus] = useState<
|
|
56
|
+
const [status, setStatus] = useState<"idle" | "loading" | "ready" | "error">("idle");
|
|
50
57
|
const [error, setError] = useState<string | null>(null);
|
|
51
58
|
const abortRef = useRef<AbortController | null>(null);
|
|
52
59
|
|
|
53
60
|
useEffect(() => {
|
|
54
|
-
if (!gatewayUrl || !
|
|
55
|
-
setStatus(
|
|
61
|
+
if (!gatewayUrl || !contentId) return;
|
|
62
|
+
setStatus("loading");
|
|
56
63
|
setError(null);
|
|
57
64
|
abortRef.current?.abort();
|
|
58
65
|
const ac = new AbortController();
|
|
@@ -60,10 +67,10 @@ export function useViewerEndpoints({ gatewayUrl, contentType, contentId, authTok
|
|
|
60
67
|
|
|
61
68
|
(async () => {
|
|
62
69
|
try {
|
|
63
|
-
const graphqlEndpoint = gatewayUrl.replace(/\/$/,
|
|
70
|
+
const graphqlEndpoint = gatewayUrl.replace(/\/$/, "");
|
|
64
71
|
const query = `
|
|
65
|
-
query ResolveViewer($
|
|
66
|
-
resolveViewerEndpoint(
|
|
72
|
+
query ResolveViewer($contentId: String!) {
|
|
73
|
+
resolveViewerEndpoint(contentId: $contentId) {
|
|
67
74
|
primary { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
68
75
|
fallbacks { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
69
76
|
metadata { contentType contentId title description durationSeconds status isLive viewers recordingSizeBytes clipSource createdAt }
|
|
@@ -71,50 +78,53 @@ export function useViewerEndpoints({ gatewayUrl, contentType, contentId, authTok
|
|
|
71
78
|
}
|
|
72
79
|
`;
|
|
73
80
|
const res = await fetchWithRetry(graphqlEndpoint, {
|
|
74
|
-
method:
|
|
81
|
+
method: "POST",
|
|
75
82
|
headers: {
|
|
76
|
-
|
|
83
|
+
"Content-Type": "application/json",
|
|
77
84
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
78
85
|
},
|
|
79
|
-
body: JSON.stringify({ query, variables: {
|
|
86
|
+
body: JSON.stringify({ query, variables: { contentId } }),
|
|
80
87
|
signal: ac.signal,
|
|
81
88
|
});
|
|
82
89
|
if (!res.ok) throw new Error(`Gateway GQL error ${res.status}`);
|
|
83
90
|
const payload = await res.json();
|
|
84
|
-
if (payload.errors?.length) throw new Error(payload.errors[0]?.message ||
|
|
91
|
+
if (payload.errors?.length) throw new Error(payload.errors[0]?.message || "GraphQL error");
|
|
85
92
|
const resp = payload.data?.resolveViewerEndpoint;
|
|
86
93
|
const primary = resp?.primary;
|
|
87
94
|
const fallbacks = Array.isArray(resp?.fallbacks) ? resp.fallbacks : [];
|
|
88
|
-
if (!primary) throw new Error(
|
|
95
|
+
if (!primary) throw new Error("No endpoints");
|
|
89
96
|
|
|
90
97
|
// Parse outputs JSON string (GraphQL returns JSON scalar as string)
|
|
91
|
-
if (primary.outputs && typeof primary.outputs ===
|
|
92
|
-
try {
|
|
98
|
+
if (primary.outputs && typeof primary.outputs === "string") {
|
|
99
|
+
try {
|
|
100
|
+
primary.outputs = JSON.parse(primary.outputs);
|
|
101
|
+
} catch {}
|
|
93
102
|
}
|
|
94
103
|
if (fallbacks) {
|
|
95
104
|
fallbacks.forEach((fb: any) => {
|
|
96
|
-
if (fb.outputs && typeof fb.outputs ===
|
|
97
|
-
try {
|
|
105
|
+
if (fb.outputs && typeof fb.outputs === "string") {
|
|
106
|
+
try {
|
|
107
|
+
fb.outputs = JSON.parse(fb.outputs);
|
|
108
|
+
} catch {}
|
|
98
109
|
}
|
|
99
110
|
});
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
setEndpoints({ primary, fallbacks, metadata: resp?.metadata });
|
|
103
|
-
setStatus(
|
|
114
|
+
setStatus("ready");
|
|
104
115
|
} catch (e) {
|
|
105
116
|
if (ac.signal.aborted) return;
|
|
106
|
-
const message = e instanceof Error ? e.message :
|
|
107
|
-
console.error(
|
|
117
|
+
const message = e instanceof Error ? e.message : "Unknown gateway error";
|
|
118
|
+
console.error("[useViewerEndpoints] Gateway resolution failed:", message);
|
|
108
119
|
setError(message);
|
|
109
|
-
setStatus(
|
|
120
|
+
setStatus("error");
|
|
110
121
|
}
|
|
111
122
|
})();
|
|
112
123
|
|
|
113
124
|
return () => ac.abort();
|
|
114
|
-
}, [gatewayUrl,
|
|
125
|
+
}, [gatewayUrl, contentId, authToken]);
|
|
115
126
|
|
|
116
127
|
return { endpoints, status, error };
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
export default useViewerEndpoints;
|
|
120
|
-
|
package/src/index.tsx
CHANGED
|
@@ -5,48 +5,56 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Main player component
|
|
8
|
-
export { default as Player } from
|
|
9
|
-
export { default as PlayerControls } from
|
|
8
|
+
export { default as Player } from "./components/Player";
|
|
9
|
+
export { default as PlayerControls } from "./components/PlayerControls";
|
|
10
10
|
|
|
11
11
|
// Overlay components
|
|
12
|
-
export { default as LoadingScreen } from
|
|
13
|
-
export { default as IdleScreen } from
|
|
14
|
-
export { default as ThumbnailOverlay } from
|
|
15
|
-
export { default as TitleOverlay } from
|
|
16
|
-
export { default as StreamStateOverlay } from
|
|
17
|
-
export { default as StatsPanel } from
|
|
18
|
-
export { default as DevModePanel } from
|
|
19
|
-
export { default as PlayerErrorBoundary } from
|
|
12
|
+
export { default as LoadingScreen } from "./components/LoadingScreen";
|
|
13
|
+
export { default as IdleScreen } from "./components/IdleScreen";
|
|
14
|
+
export { default as ThumbnailOverlay } from "./components/ThumbnailOverlay";
|
|
15
|
+
export { default as TitleOverlay } from "./components/TitleOverlay";
|
|
16
|
+
export { default as StreamStateOverlay } from "./components/StreamStateOverlay";
|
|
17
|
+
export { default as StatsPanel } from "./components/StatsPanel";
|
|
18
|
+
export { default as DevModePanel } from "./components/DevModePanel";
|
|
19
|
+
export { default as PlayerErrorBoundary } from "./components/PlayerErrorBoundary";
|
|
20
20
|
|
|
21
21
|
// Icon components
|
|
22
|
-
export * from
|
|
22
|
+
export * from "./components/Icons";
|
|
23
23
|
|
|
24
24
|
// UI primitives
|
|
25
|
-
export { Button } from
|
|
26
|
-
export { Badge } from
|
|
27
|
-
export { Slider } from
|
|
25
|
+
export { Button } from "./ui/button";
|
|
26
|
+
export { Badge } from "./ui/badge";
|
|
27
|
+
export { Slider } from "./ui/slider";
|
|
28
28
|
|
|
29
29
|
// Context
|
|
30
|
-
export {
|
|
31
|
-
|
|
30
|
+
export {
|
|
31
|
+
PlayerProvider,
|
|
32
|
+
usePlayerContext,
|
|
33
|
+
usePlayerContextOptional,
|
|
34
|
+
PlayerContext,
|
|
35
|
+
} from "./context/PlayerContext";
|
|
36
|
+
export type { PlayerContextValue } from "./context/PlayerContext";
|
|
32
37
|
|
|
33
38
|
// Hooks
|
|
34
|
-
export { useStreamState } from
|
|
35
|
-
export { usePlaybackQuality } from
|
|
36
|
-
export { useViewerEndpoints } from
|
|
37
|
-
export { useMetaTrack } from
|
|
38
|
-
export { useTelemetry } from
|
|
39
|
-
export { usePlayerSelection } from
|
|
40
|
-
export type {
|
|
41
|
-
|
|
39
|
+
export { useStreamState } from "./hooks/useStreamState";
|
|
40
|
+
export { usePlaybackQuality } from "./hooks/usePlaybackQuality";
|
|
41
|
+
export { useViewerEndpoints } from "./hooks/useViewerEndpoints";
|
|
42
|
+
export { useMetaTrack } from "./hooks/useMetaTrack";
|
|
43
|
+
export { useTelemetry } from "./hooks/useTelemetry";
|
|
44
|
+
export { usePlayerSelection } from "./hooks/usePlayerSelection";
|
|
45
|
+
export type {
|
|
46
|
+
UsePlayerSelectionOptions,
|
|
47
|
+
UsePlayerSelectionReturn,
|
|
48
|
+
} from "./hooks/usePlayerSelection";
|
|
49
|
+
export { usePlayerController } from "./hooks/usePlayerController";
|
|
42
50
|
export type {
|
|
43
51
|
UsePlayerControllerConfig,
|
|
44
52
|
UsePlayerControllerReturn,
|
|
45
53
|
PlayerControllerState,
|
|
46
|
-
} from
|
|
54
|
+
} from "./hooks/usePlayerController";
|
|
47
55
|
|
|
48
56
|
// Types
|
|
49
|
-
export * from
|
|
57
|
+
export * from "./types";
|
|
50
58
|
|
|
51
59
|
// Re-export commonly used core items
|
|
52
60
|
export {
|
|
@@ -57,7 +65,7 @@ export {
|
|
|
57
65
|
StreamStateClient,
|
|
58
66
|
QualityMonitor,
|
|
59
67
|
cn,
|
|
60
|
-
} from
|
|
68
|
+
} from "@livepeer-frameworks/player-core";
|
|
61
69
|
|
|
62
70
|
export type {
|
|
63
71
|
PlayerState,
|
|
@@ -72,4 +80,4 @@ export type {
|
|
|
72
80
|
PlayerSelection,
|
|
73
81
|
PlayerCombination,
|
|
74
82
|
PlayerManagerEvents,
|
|
75
|
-
} from
|
|
83
|
+
} from "@livepeer-frameworks/player-core";
|
package/src/types.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React-specific types for FrameWorks player
|
|
3
3
|
*/
|
|
4
|
-
import type React from
|
|
5
|
-
import type {
|
|
6
|
-
PlayerOptions,
|
|
7
|
-
PlayerState,
|
|
8
|
-
PlayerStateContext,
|
|
4
|
+
import type React from "react";
|
|
5
|
+
import type {
|
|
6
|
+
PlayerOptions,
|
|
7
|
+
PlayerState,
|
|
8
|
+
PlayerStateContext,
|
|
9
9
|
ContentEndpoints,
|
|
10
10
|
MetaTrackSubscription,
|
|
11
11
|
PlaybackQuality,
|
|
12
12
|
QualityThresholds,
|
|
13
|
-
ContentType
|
|
14
|
-
} from
|
|
13
|
+
ContentType,
|
|
14
|
+
} from "@livepeer-frameworks/player-core";
|
|
15
15
|
|
|
16
16
|
export interface PlayerProps {
|
|
17
17
|
/** Content identifier or stream name */
|
|
18
18
|
contentId: string;
|
|
19
19
|
/** Content type */
|
|
20
|
-
contentType
|
|
20
|
+
contentType?: ContentType;
|
|
21
21
|
/** Pre-resolved endpoints/capabilities from Gateway/Foghorn */
|
|
22
22
|
endpoints?: ContentEndpoints;
|
|
23
23
|
/** Optional thumbnail/poster image */
|
|
@@ -132,4 +132,4 @@ export type {
|
|
|
132
132
|
StreamStatus,
|
|
133
133
|
EndpointInfo,
|
|
134
134
|
ContentMetadata,
|
|
135
|
-
} from
|
|
135
|
+
} from "@livepeer-frameworks/player-core";
|
package/src/ui/badge.tsx
CHANGED
|
@@ -9,16 +9,17 @@ const badgeVariants = cva(
|
|
|
9
9
|
variant: {
|
|
10
10
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
11
11
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
12
|
-
outline: "border-border text-foreground"
|
|
13
|
-
}
|
|
12
|
+
outline: "border-border text-foreground",
|
|
13
|
+
},
|
|
14
14
|
},
|
|
15
15
|
defaultVariants: {
|
|
16
|
-
variant: "default"
|
|
17
|
-
}
|
|
16
|
+
variant: "default",
|
|
17
|
+
},
|
|
18
18
|
}
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
export interface BadgeProps
|
|
21
|
+
export interface BadgeProps
|
|
22
|
+
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
22
23
|
|
|
23
24
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
24
25
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
package/src/ui/button.tsx
CHANGED
|
@@ -14,32 +14,33 @@ const buttonVariants = cva(
|
|
|
14
14
|
outline: "border border-border bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
15
15
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
16
16
|
subtle: "bg-muted text-muted-foreground hover:bg-muted/80",
|
|
17
|
-
link: "text-primary underline-offset-4 hover:underline"
|
|
17
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
18
18
|
},
|
|
19
19
|
size: {
|
|
20
20
|
default: "h-10 px-4 py-2",
|
|
21
21
|
sm: "h-9 rounded-md px-3",
|
|
22
22
|
lg: "h-11 rounded-md px-8",
|
|
23
|
-
icon: "h-9 w-9"
|
|
24
|
-
}
|
|
23
|
+
icon: "h-9 w-9",
|
|
24
|
+
},
|
|
25
25
|
},
|
|
26
26
|
defaultVariants: {
|
|
27
27
|
variant: "default",
|
|
28
|
-
size: "default"
|
|
29
|
-
}
|
|
28
|
+
size: "default",
|
|
29
|
+
},
|
|
30
30
|
}
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
export interface ButtonProps
|
|
34
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
35
|
-
VariantProps<typeof buttonVariants> {
|
|
34
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
36
35
|
asChild?: boolean;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
40
39
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
41
40
|
const Comp = asChild ? Slot : "button";
|
|
42
|
-
return
|
|
41
|
+
return (
|
|
42
|
+
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
43
|
+
);
|
|
43
44
|
}
|
|
44
45
|
);
|
|
45
46
|
Button.displayName = "Button";
|