Live video on the AT Protocol
1import React, { createContext, useContext, useMemo } from "react";
2import { StoreApi, useStore } from "zustand";
3import { usePlayerContext } from "../player-store";
4import { PlayerProtocol, PlayerState } from "./player-state";
5
6// Context for a single player
7interface SinglePlayerContextType {
8 playerId: string;
9 playerStore: StoreApi<PlayerState>;
10}
11
12const SinglePlayerContext = createContext<SinglePlayerContextType | null>(null);
13
14interface SinglePlayerProviderProps {
15 children: React.ReactNode;
16 playerId?: string;
17 protocol?: PlayerProtocol;
18 rendition?: string;
19}
20
21/**
22 * Provider component for a single player that creates a scoped context
23 * This allows components to access a specific player's state without passing IDs around
24 */
25export const SinglePlayerProvider: React.FC<SinglePlayerProviderProps> = ({
26 children,
27 playerId: providedPlayerId,
28 protocol = PlayerProtocol.WEBRTC,
29 rendition = "auto",
30}) => {
31 const { players, createPlayer } = usePlayerContext();
32
33 // Create or get a player ID
34 const playerId = useMemo(() => {
35 // If a player ID is provided and exists, use it
36 if (providedPlayerId && players[providedPlayerId]) {
37 return providedPlayerId;
38 }
39
40 // If a player ID is provided but doesn't exist, create it
41 if (providedPlayerId) {
42 return createPlayer(providedPlayerId);
43 }
44
45 // Otherwise create a new player
46 return createPlayer();
47 }, [providedPlayerId, players, createPlayer]);
48
49 // Get the player store
50 const playerStore = useMemo(() => {
51 return players[playerId];
52 }, [players, playerId]);
53
54 // Set initial protocol and rendition if provided
55 React.useEffect(() => {
56 if (protocol) {
57 playerStore.setState((state) => ({
58 ...state,
59 protocol,
60 }));
61 }
62
63 if (rendition) {
64 playerStore.setState((state) => ({
65 ...state,
66 selectedRendition: rendition,
67 }));
68 }
69 }, [playerStore, protocol, rendition]);
70
71 // Create context value
72 const contextValue = useMemo(
73 () => ({
74 playerId,
75 playerStore,
76 }),
77 [playerId, playerStore],
78 );
79
80 return (
81 <SinglePlayerContext.Provider value={contextValue}>
82 {children}
83 </SinglePlayerContext.Provider>
84 );
85};
86
87/**
88 * Hook to access the current single player context
89 */
90export function useSinglePlayerContext() {
91 const context = useContext(SinglePlayerContext);
92 if (!context) {
93 throw new Error(
94 "useSinglePlayerContext must be used within a SinglePlayerProvider",
95 );
96 }
97 return context;
98}
99
100/**
101 * Hook to access the current player ID from the single player context
102 */
103export function useCurrentPlayerId(): string {
104 const { playerId } = useSinglePlayerContext();
105 return playerId;
106}
107
108/**
109 * Hook to access state from the current player without needing to specify the ID
110 */
111export function useCurrentPlayerStore<U>(
112 selector: (state: PlayerState) => U,
113): U {
114 const { playerStore } = useSinglePlayerContext();
115 return useStore(playerStore, selector);
116}
117
118/**
119 * Hook to get the protocol of the current player
120 */
121export function useCurrentPlayerProtocol(): [
122 PlayerProtocol,
123 (protocol: PlayerProtocol) => void,
124] {
125 return useCurrentPlayerStore(
126 (state) => [state.protocol, state.setProtocol] as const,
127 );
128}
129
130/**
131 * Hook to get the selected rendition of the current player
132 */
133export function useCurrentPlayerRendition(): [
134 string,
135 (rendition: string) => void,
136] {
137 return useCurrentPlayerStore(
138 (state) => [state.selectedRendition, state.setSelectedRendition] as const,
139 );
140}
141
142/**
143 * Hook to get the ingest state of the current player
144 */
145export function useCurrentPlayerIngest(): {
146 starting: boolean;
147 setStarting: (starting: boolean) => void;
148 connectionState: RTCPeerConnectionState | null;
149 setConnectionState: (state: RTCPeerConnectionState | null) => void;
150 startedTimestamp: number | null;
151 setStartedTimestamp: (timestamp: number | null) => void;
152} {
153 return useCurrentPlayerStore((state) => ({
154 starting: state.ingestStarting,
155 setStarting: state.setIngestStarting,
156 connectionState: state.ingestConnectionState,
157 setConnectionState: state.setIngestConnectionState,
158 startedTimestamp: state.ingestStarted,
159 setStartedTimestamp: state.setIngestStarted,
160 }));
161}
162
163/**
164 * HOC to wrap components with a SinglePlayerProvider
165 */
166export function withSinglePlayer<P extends object>(
167 Component: React.ComponentType<P>,
168): React.FC<P & SinglePlayerProviderProps> {
169 return function WithSinglePlayer(props: P & SinglePlayerProviderProps) {
170 const { playerId, protocol, rendition, ...componentProps } = props;
171 return (
172 <SinglePlayerProvider
173 playerId={playerId}
174 protocol={protocol}
175 rendition={rendition}
176 >
177 <Component {...(componentProps as P)} />
178 </SinglePlayerProvider>
179 );
180 };
181}