@livepeer-frameworks/player-react 0.0.3 → 0.1.0
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 +80 -0
- 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 +5 -27
- package/src/components/IdleScreen.tsx +0 -14
- package/src/components/LoadingScreen.tsx +2 -2
- package/src/components/Player.tsx +2 -18
- package/src/components/PlayerControls.tsx +2 -7
- package/src/components/SeekBar.tsx +1 -1
- package/src/components/StatsPanel.tsx +42 -20
- package/src/components/SubtitleRenderer.tsx +1 -1
- package/src/hooks/useMetaTrack.ts +2 -2
- package/src/hooks/usePlayerController.ts +3 -5
- package/src/hooks/useStreamState.ts +2 -2
- package/src/hooks/useTelemetry.ts +1 -1
- package/src/hooks/useViewerEndpoints.ts +7 -8
- package/src/types.ts +1 -1
- package/src/ui/slider.tsx +1 -1
|
@@ -55,6 +55,8 @@ interface PlayerControlsProps {
|
|
|
55
55
|
isLoopEnabled?: boolean;
|
|
56
56
|
/** Toggle loop callback */
|
|
57
57
|
onToggleLoop?: () => void;
|
|
58
|
+
/** Jump to live edge callback */
|
|
59
|
+
onJumpToLive?: () => void;
|
|
58
60
|
}
|
|
59
61
|
declare const PlayerControls: React.FC<PlayerControlsProps>;
|
|
60
62
|
export default PlayerControls;
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { type ContentMetadata, type PlaybackQuality } from "@livepeer-frameworks/player-core";
|
|
3
|
-
interface StreamStateInfo {
|
|
4
|
-
status?: string;
|
|
5
|
-
viewers?: number;
|
|
6
|
-
tracks?: Array<{
|
|
7
|
-
type: string;
|
|
8
|
-
codec: string;
|
|
9
|
-
width?: number;
|
|
10
|
-
height?: number;
|
|
11
|
-
bps?: number;
|
|
12
|
-
channels?: number;
|
|
13
|
-
}>;
|
|
14
|
-
}
|
|
2
|
+
import { type ContentMetadata, type PlaybackQuality, type StreamState } from "@livepeer-frameworks/player-core";
|
|
15
3
|
interface StatsPanelProps {
|
|
16
4
|
isOpen: boolean;
|
|
17
5
|
onClose: () => void;
|
|
18
6
|
metadata?: ContentMetadata | null;
|
|
19
|
-
streamState?:
|
|
7
|
+
streamState?: StreamState | null;
|
|
20
8
|
quality?: PlaybackQuality | null;
|
|
21
9
|
videoElement?: HTMLVideoElement | null;
|
|
22
10
|
protocol?: string;
|
|
@@ -33,7 +33,7 @@ export interface UseMetaTrackReturn {
|
|
|
33
33
|
* ```tsx
|
|
34
34
|
* const { isConnected, subscribe } = useMetaTrack({
|
|
35
35
|
* mistBaseUrl: 'https://mist.example.com',
|
|
36
|
-
* streamName: '
|
|
36
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
37
37
|
* enabled: true,
|
|
38
38
|
* });
|
|
39
39
|
*
|
|
@@ -117,6 +117,8 @@ export interface UsePlayerControllerReturn {
|
|
|
117
117
|
seek: (time: number) => void;
|
|
118
118
|
/** Seek by delta */
|
|
119
119
|
seekBy: (delta: number) => void;
|
|
120
|
+
/** Jump to live edge (for live streams) */
|
|
121
|
+
jumpToLive: () => void;
|
|
120
122
|
/** Set volume */
|
|
121
123
|
setVolume: (volume: number) => void;
|
|
122
124
|
/** Toggle mute */
|
|
@@ -10,7 +10,7 @@ import type { UseStreamStateOptions, StreamState } from '../types';
|
|
|
10
10
|
* ```tsx
|
|
11
11
|
* const { status, isOnline, message } = useStreamState({
|
|
12
12
|
* mistBaseUrl: 'https://mist.example.com',
|
|
13
|
-
* streamName: '
|
|
13
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
14
14
|
* pollInterval: 3000,
|
|
15
15
|
* });
|
|
16
16
|
* ```
|
|
@@ -33,7 +33,7 @@ export interface UseTelemetryOptions extends TelemetryOptions {
|
|
|
33
33
|
* endpoint: '/api/telemetry',
|
|
34
34
|
* interval: 5000,
|
|
35
35
|
* videoElement,
|
|
36
|
-
* contentId: '
|
|
36
|
+
* contentId: 'pk_...', // playbackId (view key)
|
|
37
37
|
* contentType: 'live',
|
|
38
38
|
* playerType: 'hlsjs',
|
|
39
39
|
* protocol: 'HLS',
|
|
@@ -2,11 +2,11 @@ import type { ContentType } from '@livepeer-frameworks/player-core';
|
|
|
2
2
|
import type { ContentEndpoints } from '../types';
|
|
3
3
|
interface Params {
|
|
4
4
|
gatewayUrl: string;
|
|
5
|
-
contentType: ContentType;
|
|
6
5
|
contentId: string;
|
|
6
|
+
contentType?: ContentType;
|
|
7
7
|
authToken?: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function useViewerEndpoints({ gatewayUrl, contentType, contentId, authToken }: Params): {
|
|
9
|
+
export declare function useViewerEndpoints({ gatewayUrl, contentType: _contentType, contentId, authToken }: Params): {
|
|
10
10
|
endpoints: ContentEndpoints | null;
|
|
11
11
|
status: "error" | "ready" | "idle" | "loading";
|
|
12
12
|
error: string | null;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface PlayerProps {
|
|
|
7
7
|
/** Content identifier or stream name */
|
|
8
8
|
contentId: string;
|
|
9
9
|
/** Content type */
|
|
10
|
-
contentType
|
|
10
|
+
contentType?: ContentType;
|
|
11
11
|
/** Pre-resolved endpoints/capabilities from Gateway/Foghorn */
|
|
12
12
|
endpoints?: ContentEndpoints;
|
|
13
13
|
/** Optional thumbnail/poster image */
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
4
|
variant?: "link" | "default" | "secondary" | "ghost" | "outline" | "destructive" | "subtle" | null | undefined;
|
|
5
|
-
size?: "default" | "
|
|
5
|
+
size?: "default" | "icon" | "sm" | "lg" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
8
|
asChild?: boolean;
|
package/package.json
CHANGED
|
@@ -76,7 +76,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
|
|
|
76
76
|
videoElement,
|
|
77
77
|
protocol,
|
|
78
78
|
nodeId,
|
|
79
|
-
isVisible = true,
|
|
79
|
+
isVisible: _isVisible = true,
|
|
80
80
|
isOpen: controlledIsOpen,
|
|
81
81
|
onOpenChange,
|
|
82
82
|
}) => {
|
|
@@ -91,7 +91,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
|
|
|
91
91
|
}
|
|
92
92
|
}, [onOpenChange]);
|
|
93
93
|
const [activeTab, setActiveTab] = useState<"config" | "stats">("config");
|
|
94
|
-
const [
|
|
94
|
+
const [, setCurrentComboIndex] = useState(0);
|
|
95
95
|
const [hoveredComboIndex, setHoveredComboIndex] = useState<number | null>(null);
|
|
96
96
|
const [tooltipAbove, setTooltipAbove] = useState(false);
|
|
97
97
|
const [showDisabledPlayers, setShowDisabledPlayers] = useState(false);
|
|
@@ -147,7 +147,7 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
|
|
|
147
147
|
setPlayerStats(stats);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
} catch
|
|
150
|
+
} catch {
|
|
151
151
|
// Ignore errors
|
|
152
152
|
}
|
|
153
153
|
};
|
|
@@ -274,31 +274,9 @@ const DevModePanel: React.FC<DevModePanelProps> = ({
|
|
|
274
274
|
return () => clearInterval(interval);
|
|
275
275
|
}, [isOpen, activeTab, videoElement]);
|
|
276
276
|
|
|
277
|
-
//
|
|
277
|
+
// Panel is only rendered when open (no floating toggle button)
|
|
278
278
|
if (!isOpen) {
|
|
279
|
-
return
|
|
280
|
-
<button
|
|
281
|
-
type="button"
|
|
282
|
-
onClick={() => setIsOpen(true)}
|
|
283
|
-
className={cn(
|
|
284
|
-
"fw-dev-toggle",
|
|
285
|
-
!isVisible && "fw-dev-toggle--hidden"
|
|
286
|
-
)}
|
|
287
|
-
title="Advanced Settings"
|
|
288
|
-
aria-label="Open advanced settings panel"
|
|
289
|
-
>
|
|
290
|
-
<svg
|
|
291
|
-
width="16"
|
|
292
|
-
height="16"
|
|
293
|
-
viewBox="0 0 24 24"
|
|
294
|
-
fill="none"
|
|
295
|
-
stroke="currentColor"
|
|
296
|
-
strokeWidth="2"
|
|
297
|
-
>
|
|
298
|
-
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
|
299
|
-
</svg>
|
|
300
|
-
</button>
|
|
301
|
-
);
|
|
279
|
+
return null;
|
|
302
280
|
}
|
|
303
281
|
|
|
304
282
|
return (
|
|
@@ -210,20 +210,6 @@ interface StatusOverlayProps {
|
|
|
210
210
|
onRetry?: () => void;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
function getStatusLabel(status?: StreamStatus): string {
|
|
214
|
-
switch (status) {
|
|
215
|
-
case 'ONLINE': return 'ONLINE';
|
|
216
|
-
case 'OFFLINE': return 'OFFLINE';
|
|
217
|
-
case 'INITIALIZING': return 'STARTING';
|
|
218
|
-
case 'BOOTING': return 'STARTING';
|
|
219
|
-
case 'WAITING_FOR_DATA': return 'WAITING';
|
|
220
|
-
case 'SHUTTING_DOWN': return 'ENDING';
|
|
221
|
-
case 'ERROR': return 'ERROR';
|
|
222
|
-
case 'INVALID': return 'ERROR';
|
|
223
|
-
default: return 'CONNECTING';
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
213
|
function StatusIcon({ status }: { status?: StreamStatus }) {
|
|
228
214
|
const iconClass = "w-5 h-5";
|
|
229
215
|
|
|
@@ -297,7 +297,7 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ message = "Waiting for so
|
|
|
297
297
|
// Fallback to synthetic sound if data URL fails
|
|
298
298
|
createSyntheticHitmarkerSound();
|
|
299
299
|
});
|
|
300
|
-
} catch
|
|
300
|
+
} catch {
|
|
301
301
|
// Fallback to synthetic sound
|
|
302
302
|
createSyntheticHitmarkerSound();
|
|
303
303
|
}
|
|
@@ -372,7 +372,7 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ message = "Waiting for so
|
|
|
372
372
|
oscillator2.stop(stopTime);
|
|
373
373
|
noiseSource.stop(startTime + 0.02);
|
|
374
374
|
|
|
375
|
-
} catch
|
|
375
|
+
} catch {
|
|
376
376
|
console.log('Audio context not available');
|
|
377
377
|
}
|
|
378
378
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React, { useState, useCallback
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
2
|
import IdleScreen from "./IdleScreen";
|
|
3
|
-
import ThumbnailOverlay from "./ThumbnailOverlay";
|
|
4
3
|
import TitleOverlay from "./TitleOverlay";
|
|
5
4
|
import StatsPanel from "./StatsPanel";
|
|
6
5
|
import PlayerControls from "./PlayerControls";
|
|
@@ -134,9 +133,6 @@ const PlayerInner: React.FC<PlayerProps> = ({
|
|
|
134
133
|
const showBufferingSpinner = !state.shouldShowIdleScreen &&
|
|
135
134
|
state.isBuffering && !state.error && state.hasPlaybackStarted;
|
|
136
135
|
|
|
137
|
-
// Click-to-play overlay support
|
|
138
|
-
const supportsOverlay = false;
|
|
139
|
-
|
|
140
136
|
// ============================================================================
|
|
141
137
|
// Waiting for Endpoint (shown as overlay, not early return)
|
|
142
138
|
// ============================================================================
|
|
@@ -182,19 +178,7 @@ const PlayerInner: React.FC<PlayerProps> = ({
|
|
|
182
178
|
isOpen={isStatsOpen}
|
|
183
179
|
onClose={handleStatsToggle}
|
|
184
180
|
metadata={state.metadata}
|
|
185
|
-
streamState={state.streamState
|
|
186
|
-
status: state.streamState.status,
|
|
187
|
-
viewers: state.metadata?.viewers,
|
|
188
|
-
tracks: state.streamState.streamInfo?.meta?.tracks
|
|
189
|
-
? Object.values(state.streamState.streamInfo.meta.tracks).map(t => ({
|
|
190
|
-
type: t.type,
|
|
191
|
-
codec: t.codec,
|
|
192
|
-
width: t.width,
|
|
193
|
-
height: t.height,
|
|
194
|
-
bps: t.bps,
|
|
195
|
-
}))
|
|
196
|
-
: [],
|
|
197
|
-
} : null}
|
|
181
|
+
streamState={state.streamState}
|
|
198
182
|
quality={state.playbackQuality}
|
|
199
183
|
videoElement={state.videoElement}
|
|
200
184
|
protocol={primaryEndpoint?.protocol}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
cn,
|
|
5
5
|
// Seeking utilities from core
|
|
6
6
|
SPEED_PRESETS,
|
|
7
|
-
getLatencyTier,
|
|
8
7
|
isMediaStreamSource,
|
|
9
8
|
supportsPlaybackRate as coreSupportsPlaybackRate,
|
|
10
9
|
calculateSeekableRange,
|
|
@@ -13,20 +12,16 @@ import {
|
|
|
13
12
|
calculateIsNearLive,
|
|
14
13
|
isLiveContent,
|
|
15
14
|
// Time formatting from core
|
|
16
|
-
formatTime,
|
|
17
15
|
formatTimeDisplay,
|
|
18
16
|
} from "@livepeer-frameworks/player-core";
|
|
19
17
|
import { Slider } from "../ui/slider";
|
|
20
18
|
import SeekBar from "./SeekBar";
|
|
21
19
|
import {
|
|
22
|
-
ClosedCaptionsIcon,
|
|
23
20
|
FullscreenToggleIcon,
|
|
24
|
-
LiveIcon,
|
|
25
21
|
PlayPauseIcon,
|
|
26
22
|
SeekToLiveIcon,
|
|
27
23
|
SkipBackIcon,
|
|
28
24
|
SkipForwardIcon,
|
|
29
|
-
StatsIcon,
|
|
30
25
|
VolumeIcon,
|
|
31
26
|
SettingsIcon
|
|
32
27
|
} from "./Icons";
|
|
@@ -107,8 +102,8 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
107
102
|
onTogglePlay,
|
|
108
103
|
onToggleFullscreen,
|
|
109
104
|
isFullscreen: propIsFullscreen,
|
|
110
|
-
isLoopEnabled:
|
|
111
|
-
onToggleLoop,
|
|
105
|
+
isLoopEnabled: _propIsLoopEnabled,
|
|
106
|
+
onToggleLoop: _onToggleLoop,
|
|
112
107
|
onJumpToLive,
|
|
113
108
|
}) => {
|
|
114
109
|
// Context fallback - prefer props passed from parent over context
|
|
@@ -145,7 +145,7 @@ const SeekBar: React.FC<SeekBarProps> = ({
|
|
|
145
145
|
|
|
146
146
|
// Fallback: If we have liveEdge, use it even if not marked as live
|
|
147
147
|
// This handles cases where duration is Infinity but we have valid seekable data
|
|
148
|
-
if (Number.isFinite(liveEdge) && liveEdge > 0) {
|
|
148
|
+
if (liveEdge !== undefined && Number.isFinite(liveEdge) && liveEdge > 0) {
|
|
149
149
|
const start = Number.isFinite(seekableStart) ? seekableStart : 0;
|
|
150
150
|
const window = liveEdge - start;
|
|
151
151
|
if (window > 0) {
|
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type: string;
|
|
9
|
-
codec: string;
|
|
10
|
-
width?: number;
|
|
11
|
-
height?: number;
|
|
12
|
-
bps?: number;
|
|
13
|
-
channels?: number;
|
|
14
|
-
}>;
|
|
15
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
cn,
|
|
4
|
+
type ContentMetadata,
|
|
5
|
+
type PlaybackQuality,
|
|
6
|
+
type StreamState,
|
|
7
|
+
} from "@livepeer-frameworks/player-core";
|
|
16
8
|
|
|
17
9
|
interface StatsPanelProps {
|
|
18
10
|
isOpen: boolean;
|
|
19
11
|
onClose: () => void;
|
|
20
12
|
metadata?: ContentMetadata | null;
|
|
21
|
-
streamState?:
|
|
13
|
+
streamState?: StreamState | null;
|
|
22
14
|
quality?: PlaybackQuality | null;
|
|
23
15
|
videoElement?: HTMLVideoElement | null;
|
|
24
16
|
protocol?: string;
|
|
@@ -61,20 +53,46 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
|
|
|
61
53
|
const latency = quality?.latency ? `${Math.round(quality.latency)} ms` : "—";
|
|
62
54
|
|
|
63
55
|
// Stream state stats
|
|
64
|
-
const viewers =
|
|
56
|
+
const viewers = metadata?.viewers ?? "—";
|
|
65
57
|
const streamStatus = streamState?.status ?? metadata?.status ?? "—";
|
|
66
58
|
|
|
59
|
+
const mistInfo = metadata?.mist ?? streamState?.streamInfo;
|
|
60
|
+
|
|
61
|
+
const deriveTracksFromMist = () => {
|
|
62
|
+
const mistTracks = mistInfo?.meta?.tracks;
|
|
63
|
+
if (!mistTracks) return undefined;
|
|
64
|
+
return Object.values(mistTracks).map(t => ({
|
|
65
|
+
type: t.type,
|
|
66
|
+
codec: t.codec,
|
|
67
|
+
width: t.width,
|
|
68
|
+
height: t.height,
|
|
69
|
+
bitrate: typeof t.bps === "number" ? Math.round(t.bps) : undefined,
|
|
70
|
+
fps: typeof t.fpks === "number" ? t.fpks / 1000 : undefined,
|
|
71
|
+
channels: t.channels,
|
|
72
|
+
sampleRate: t.rate,
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
75
|
+
|
|
67
76
|
// Format track info from metadata
|
|
68
77
|
const formatTracks = () => {
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
const tracks = metadata?.tracks ?? deriveTracksFromMist();
|
|
79
|
+
if (!tracks?.length) return "—";
|
|
80
|
+
return tracks.map(t => {
|
|
71
81
|
if (t.type === "video") {
|
|
72
|
-
|
|
82
|
+
const resolution = t.width && t.height ? `${t.width}x${t.height}` : "?";
|
|
83
|
+
const bitrate = t.bitrate ? `${Math.round(t.bitrate / 1000)}kbps` : "?";
|
|
84
|
+
return `${t.codec ?? "?"} ${resolution}@${bitrate}`;
|
|
73
85
|
}
|
|
74
|
-
|
|
86
|
+
const channels = t.channels ? `${t.channels}ch` : "?";
|
|
87
|
+
return `${t.codec ?? "?"} ${channels}`;
|
|
75
88
|
}).join(", ");
|
|
76
89
|
};
|
|
77
90
|
|
|
91
|
+
const mistType = mistInfo?.type ?? "—";
|
|
92
|
+
const mistBufferWindow = mistInfo?.meta?.buffer_window;
|
|
93
|
+
const mistLastMs = mistInfo?.lastms;
|
|
94
|
+
const mistUnixOffset = mistInfo?.unixoffset;
|
|
95
|
+
|
|
78
96
|
const stats = [
|
|
79
97
|
{ label: "Resolution", value: currentRes },
|
|
80
98
|
{ label: "Buffer", value: `${buffered}s` },
|
|
@@ -90,6 +108,10 @@ const StatsPanel: React.FC<StatsPanelProps> = ({
|
|
|
90
108
|
{ label: "Viewers", value: String(viewers) },
|
|
91
109
|
{ label: "Status", value: streamStatus },
|
|
92
110
|
{ label: "Tracks", value: formatTracks() },
|
|
111
|
+
{ label: "Mist Type", value: mistType },
|
|
112
|
+
{ label: "Mist Buffer Window", value: mistBufferWindow != null ? String(mistBufferWindow) : "—" },
|
|
113
|
+
{ label: "Mist Lastms", value: mistLastMs != null ? String(mistLastMs) : "—" },
|
|
114
|
+
{ label: "Mist Unixoffset", value: mistUnixOffset != null ? String(mistUnixOffset) : "—" },
|
|
93
115
|
];
|
|
94
116
|
|
|
95
117
|
// Add metadata fields if available
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
2
|
-
import { MetaTrackManager, type
|
|
2
|
+
import { MetaTrackManager, type MetaTrackEvent } from '@livepeer-frameworks/player-core';
|
|
3
3
|
import type { UseMetaTrackOptions } from '../types';
|
|
4
4
|
|
|
5
5
|
export interface UseMetaTrackReturn {
|
|
@@ -36,7 +36,7 @@ export interface UseMetaTrackReturn {
|
|
|
36
36
|
* ```tsx
|
|
37
37
|
* const { isConnected, subscribe } = useMetaTrack({
|
|
38
38
|
* mistBaseUrl: 'https://mist.example.com',
|
|
39
|
-
* streamName: '
|
|
39
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
40
40
|
* enabled: true,
|
|
41
41
|
* });
|
|
42
42
|
*
|
|
@@ -5,19 +5,16 @@
|
|
|
5
5
|
* Manages the complete player lifecycle and provides reactive state.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useEffect, useRef, useCallback
|
|
8
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
9
9
|
import {
|
|
10
10
|
PlayerController,
|
|
11
11
|
type PlayerControllerConfig,
|
|
12
|
-
type PlayerControllerEvents,
|
|
13
12
|
type PlayerState,
|
|
14
13
|
type StreamState,
|
|
15
|
-
type StreamSource,
|
|
16
14
|
type StreamInfo,
|
|
17
15
|
type PlaybackQuality,
|
|
18
16
|
type ContentEndpoints,
|
|
19
17
|
type ContentMetadata,
|
|
20
|
-
type MistStreamInfo,
|
|
21
18
|
} from '@livepeer-frameworks/player-core';
|
|
22
19
|
|
|
23
20
|
// ============================================================================
|
|
@@ -273,6 +270,7 @@ export function usePlayerController(
|
|
|
273
270
|
setState(prev => ({
|
|
274
271
|
...prev,
|
|
275
272
|
streamState,
|
|
273
|
+
metadata: controller.getMetadata(),
|
|
276
274
|
isEffectivelyLive: controller.isEffectivelyLive(),
|
|
277
275
|
shouldShowIdleScreen: controller.shouldShowIdleScreen(),
|
|
278
276
|
}));
|
|
@@ -329,7 +327,7 @@ export function usePlayerController(
|
|
|
329
327
|
});
|
|
330
328
|
}));
|
|
331
329
|
|
|
332
|
-
unsubs.push(controller.on('playerSelected', ({ player, source }) => {
|
|
330
|
+
unsubs.push(controller.on('playerSelected', ({ player: _player, source }) => {
|
|
333
331
|
setState(prev => ({
|
|
334
332
|
...prev,
|
|
335
333
|
currentPlayerInfo: controller.getCurrentPlayerInfo(),
|
|
@@ -70,7 +70,7 @@ const initialState: StreamState = {
|
|
|
70
70
|
* ```tsx
|
|
71
71
|
* const { status, isOnline, message } = useStreamState({
|
|
72
72
|
* mistBaseUrl: 'https://mist.example.com',
|
|
73
|
-
* streamName: '
|
|
73
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
74
74
|
* pollInterval: 3000,
|
|
75
75
|
* });
|
|
76
76
|
* ```
|
|
@@ -264,7 +264,7 @@ export function useStreamState(options: UseStreamStateOptions): UseStreamStateRe
|
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
266
|
|
|
267
|
-
ws.onerror = (
|
|
267
|
+
ws.onerror = (_event) => {
|
|
268
268
|
console.warn('[useStreamState] WebSocket error, falling back to HTTP polling');
|
|
269
269
|
if (wsTimeoutRef.current) {
|
|
270
270
|
clearTimeout(wsTimeoutRef.current);
|
|
@@ -36,7 +36,7 @@ export interface UseTelemetryOptions extends TelemetryOptions {
|
|
|
36
36
|
* endpoint: '/api/telemetry',
|
|
37
37
|
* interval: 5000,
|
|
38
38
|
* videoElement,
|
|
39
|
-
* contentId: '
|
|
39
|
+
* contentId: 'pk_...', // playbackId (view key)
|
|
40
40
|
* contentType: 'live',
|
|
41
41
|
* playerType: 'hlsjs',
|
|
42
42
|
* protocol: 'HLS',
|
|
@@ -7,8 +7,8 @@ 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
|
|
|
@@ -44,14 +44,14 @@ async function fetchWithRetry(
|
|
|
44
44
|
throw lastError ?? new Error('Gateway unreachable after retries');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function useViewerEndpoints({ gatewayUrl, contentType, contentId, authToken }: Params) {
|
|
47
|
+
export function useViewerEndpoints({ gatewayUrl, contentType: _contentType, contentId, authToken }: Params) {
|
|
48
48
|
const [endpoints, setEndpoints] = useState<ContentEndpoints | null>(null);
|
|
49
49
|
const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
|
|
50
50
|
const [error, setError] = useState<string | null>(null);
|
|
51
51
|
const abortRef = useRef<AbortController | null>(null);
|
|
52
52
|
|
|
53
53
|
useEffect(() => {
|
|
54
|
-
if (!gatewayUrl || !
|
|
54
|
+
if (!gatewayUrl || !contentId) return;
|
|
55
55
|
setStatus('loading');
|
|
56
56
|
setError(null);
|
|
57
57
|
abortRef.current?.abort();
|
|
@@ -62,8 +62,8 @@ export function useViewerEndpoints({ gatewayUrl, contentType, contentId, authTok
|
|
|
62
62
|
try {
|
|
63
63
|
const graphqlEndpoint = gatewayUrl.replace(/\/$/, '');
|
|
64
64
|
const query = `
|
|
65
|
-
query ResolveViewer($
|
|
66
|
-
resolveViewerEndpoint(
|
|
65
|
+
query ResolveViewer($contentId: String!) {
|
|
66
|
+
resolveViewerEndpoint(contentId: $contentId) {
|
|
67
67
|
primary { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
68
68
|
fallbacks { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
69
69
|
metadata { contentType contentId title description durationSeconds status isLive viewers recordingSizeBytes clipSource createdAt }
|
|
@@ -76,7 +76,7 @@ export function useViewerEndpoints({ gatewayUrl, contentType, contentId, authTok
|
|
|
76
76
|
'Content-Type': 'application/json',
|
|
77
77
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
78
78
|
},
|
|
79
|
-
body: JSON.stringify({ query, variables: {
|
|
79
|
+
body: JSON.stringify({ query, variables: { contentId } }),
|
|
80
80
|
signal: ac.signal,
|
|
81
81
|
});
|
|
82
82
|
if (!res.ok) throw new Error(`Gateway GQL error ${res.status}`);
|
|
@@ -111,10 +111,9 @@ export function useViewerEndpoints({ gatewayUrl, contentType, contentId, authTok
|
|
|
111
111
|
})();
|
|
112
112
|
|
|
113
113
|
return () => ac.abort();
|
|
114
|
-
}, [gatewayUrl,
|
|
114
|
+
}, [gatewayUrl, contentId, authToken]);
|
|
115
115
|
|
|
116
116
|
return { endpoints, status, error };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export default useViewerEndpoints;
|
|
120
|
-
|
package/src/types.ts
CHANGED
|
@@ -17,7 +17,7 @@ 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 */
|
package/src/ui/slider.tsx
CHANGED
|
@@ -13,7 +13,7 @@ export interface SliderProps extends React.ComponentPropsWithoutRef<typeof Slide
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
|
16
|
-
({ className, trackClassName, thumbClassName, showTrack = true, hoverThumb = false, accentColor = false, orientation = "horizontal", ...props }, ref) => {
|
|
16
|
+
({ className, trackClassName, thumbClassName, showTrack = true, hoverThumb: _hoverThumb = false, accentColor = false, orientation = "horizontal", ...props }, ref) => {
|
|
17
17
|
// Colors based on accentColor prop
|
|
18
18
|
const rangeColorClass = accentColor ? "bg-[hsl(var(--tn-cyan,195_100%_50%))]" : "bg-white/90";
|
|
19
19
|
const thumbColorClass = accentColor ? "bg-[hsl(var(--tn-cyan,195_100%_50%))]" : "bg-white";
|