From 7f41c71b350cfec6bea2b0f6dbf64c755eace356 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Thu, 24 Aug 2023 13:54:15 +0200 Subject: [PATCH] feat(egress-composite): new configuration system (#689) Adds `ConfigurationContext` for configuration data provisioning across the whole application. To load the application append this script to the page `head`: ```html ``` Accepted configuration values (subject to change): https://github.com/GetStream/stream-video-js/blob/7d5d435634a0a4e601cf6b35f289a709f09a2bdb/sample-apps/react/egress-composite/src/ConfigurationContext.tsx#L4-L56 --------- Co-authored-by: Oliver Lazoroski Co-authored-by: Tommaso Barbugli --- .../workflows/deploy-react-sample-apps.yml | 2 +- .../react/egress-composite/.env-example | 2 +- .../react/egress-composite/package.json | 1 + .../egress-composite/src/CompositeApp.scss | 2 +- .../egress-composite/src/CompositeApp.tsx | 57 +++++++------- .../src/ConfigurationContext.tsx | 74 +++++++++++++++++++ .../src/components/LogoAndTitleOverlay.tsx | 36 +++++++++ .../src/components/UIDispatcher.tsx | 23 ++++++ .../egress-composite/src/components/index.ts | 2 + .../layouts/DominantSpeaker}/AudioTracks.tsx | 2 +- .../DominantSpeaker/DominantSpeaker.scss} | 2 +- .../DominantSpeaker}/DominantSpeaker.tsx | 4 +- .../DominantSpeakerScreenShare.scss} | 6 +- .../DominantSpeakerScreenShare.tsx | 8 +- .../layouts/DominantSpeaker/index.ts | 2 + .../useSpotlightParticipant.ts | 10 ++- .../layouts/PaginatedGrid/PaginatedGrid.scss} | 2 +- .../layouts/PaginatedGrid/PaginatedGrid.tsx | 29 ++++++++ .../components/layouts/PaginatedGrid/index.ts | 1 + .../layouts/Spotlight/Spotlight.scss | 12 +++ .../layouts/Spotlight/Spotlight.tsx | 33 +++++++++ .../src/components/layouts/Spotlight/index.ts | 1 + .../{ => components}/layouts/egressReady.ts | 2 +- .../src/components/layouts/index.ts | 19 +++++ .../src/hooks/useAppConfig.ts | 42 ----------- .../src/layouts/dominant-speaker/index.ts | 10 --- .../src/layouts/grid/GridView.tsx | 25 ------- .../src/layouts/grid/index.ts | 11 --- .../egress-composite/src/layouts/index.ts | 20 ----- .../react/egress-composite/src/main.tsx | 55 +++++++++++++- 30 files changed, 337 insertions(+), 158 deletions(-) create mode 100644 sample-apps/react/egress-composite/src/ConfigurationContext.tsx create mode 100644 sample-apps/react/egress-composite/src/components/LogoAndTitleOverlay.tsx create mode 100644 sample-apps/react/egress-composite/src/components/UIDispatcher.tsx create mode 100644 sample-apps/react/egress-composite/src/components/index.ts rename sample-apps/react/egress-composite/src/{layouts/dominant-speaker => components/layouts/DominantSpeaker}/AudioTracks.tsx (92%) rename sample-apps/react/egress-composite/src/{layouts/dominant-speaker/Spotlight.scss => components/layouts/DominantSpeaker/DominantSpeaker.scss} (85%) rename sample-apps/react/egress-composite/src/{layouts/dominant-speaker => components/layouts/DominantSpeaker}/DominantSpeaker.tsx (93%) rename sample-apps/react/egress-composite/src/{layouts/dominant-speaker/ScreenShare.scss => components/layouts/DominantSpeaker/DominantSpeakerScreenShare.scss} (54%) rename sample-apps/react/egress-composite/src/{layouts/dominant-speaker => components/layouts/DominantSpeaker}/DominantSpeakerScreenShare.tsx (86%) create mode 100644 sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/index.ts rename sample-apps/react/egress-composite/src/{layouts/dominant-speaker => components/layouts/DominantSpeaker}/useSpotlightParticipant.ts (81%) rename sample-apps/react/egress-composite/src/{layouts/grid/GridView.scss => components/layouts/PaginatedGrid/PaginatedGrid.scss} (93%) create mode 100644 sample-apps/react/egress-composite/src/components/layouts/PaginatedGrid/PaginatedGrid.tsx create mode 100644 sample-apps/react/egress-composite/src/components/layouts/PaginatedGrid/index.ts create mode 100644 sample-apps/react/egress-composite/src/components/layouts/Spotlight/Spotlight.scss create mode 100644 sample-apps/react/egress-composite/src/components/layouts/Spotlight/Spotlight.tsx create mode 100644 sample-apps/react/egress-composite/src/components/layouts/Spotlight/index.ts rename sample-apps/react/egress-composite/src/{ => components}/layouts/egressReady.ts (95%) create mode 100644 sample-apps/react/egress-composite/src/components/layouts/index.ts delete mode 100644 sample-apps/react/egress-composite/src/hooks/useAppConfig.ts delete mode 100644 sample-apps/react/egress-composite/src/layouts/dominant-speaker/index.ts delete mode 100644 sample-apps/react/egress-composite/src/layouts/grid/GridView.tsx delete mode 100644 sample-apps/react/egress-composite/src/layouts/grid/index.ts delete mode 100644 sample-apps/react/egress-composite/src/layouts/index.ts diff --git a/.github/workflows/deploy-react-sample-apps.yml b/.github/workflows/deploy-react-sample-apps.yml index 3d77d186c5..bd83b08348 100644 --- a/.github/workflows/deploy-react-sample-apps.yml +++ b/.github/workflows/deploy-react-sample-apps.yml @@ -48,7 +48,7 @@ jobs: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ matrix.application.project-id }} VITE_STREAM_API_KEY: ${{ vars.EGRESS_STREAM_API_KEY }} - VITE_STREAM_TOKEN: ${{ secrets.EGRESS_USER_TOKEN }} + VITE_STREAM_USER_TOKEN: ${{ secrets.EGRESS_USER_TOKEN }} VITE_STREAM_KEY: ${{ vars.STREAM_API_KEY_SAMPLE_APPS }} VITE_STREAM_SECRET: ${{ secrets.STREAM_SECRET_SAMPLE_APPS }} VITE_VIDEO_DEMO_SENTRY_DNS: ${{secrets.VIDEO_DEMO_SENTRY_DNS}} diff --git a/sample-apps/react/egress-composite/.env-example b/sample-apps/react/egress-composite/.env-example index 69eb913b7c..02ce8040e0 100644 --- a/sample-apps/react/egress-composite/.env-example +++ b/sample-apps/react/egress-composite/.env-example @@ -1,2 +1,2 @@ VITE_STREAM_API_KEY="" -VITE_STREAM_TOKEN="" +VITE_STREAM_USER_TOKEN="" diff --git a/sample-apps/react/egress-composite/package.json b/sample-apps/react/egress-composite/package.json index a3e808efa4..de86946e0a 100644 --- a/sample-apps/react/egress-composite/package.json +++ b/sample-apps/react/egress-composite/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "start": "vite", + "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, diff --git a/sample-apps/react/egress-composite/src/CompositeApp.scss b/sample-apps/react/egress-composite/src/CompositeApp.scss index b7d394107b..8922fbdd31 100644 --- a/sample-apps/react/egress-composite/src/CompositeApp.scss +++ b/sample-apps/react/egress-composite/src/CompositeApp.scss @@ -13,6 +13,7 @@ .str-video { color: var(--str-video__text-color1); + position: relative; } body { @@ -24,7 +25,6 @@ body { } #root { - max-width: 1920px; margin: 0 auto; text-align: center; } diff --git a/sample-apps/react/egress-composite/src/CompositeApp.tsx b/sample-apps/react/egress-composite/src/CompositeApp.tsx index 5448568124..62f628d8d4 100644 --- a/sample-apps/react/egress-composite/src/CompositeApp.tsx +++ b/sample-apps/react/egress-composite/src/CompositeApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { PropsWithChildren, useEffect, useState } from 'react'; import { Call, CallingState, @@ -7,41 +7,50 @@ import { StreamTheme, StreamVideo, StreamVideoClient, - useCallStateHooks, } from '@stream-io/video-react-sdk'; -import Layouts, { DEFAULT_LAYOUT_ID, LayoutId } from './layouts'; -import { useAppConfig } from './hooks/useAppConfig'; + +import { useConfigurationContext } from './ConfigurationContext'; import { EgressReadyNotificationProvider } from './hooks/useNotifyEgress'; + import './CompositeApp.scss'; +import { UIDispatcher, LogoAndTitleOverlay } from './components'; export const CompositeApp = () => { - const config = useAppConfig(); + const { + base_url: baseURL, + api_key: apiKey, + user_id: userId, + call_type: callType, + call_id: callId, + token, + } = useConfigurationContext(); + const options: StreamClientOptions = {}; - if (config.baseURL) { - options.baseURL = config.baseURL; + if (baseURL) { + options.baseURL = baseURL; } const [client] = useState( - () => new StreamVideoClient(config.apiKey, options), + () => new StreamVideoClient(apiKey, options), ); useEffect(() => { client.connectUser( { - id: config.userId, + id: userId, }, - config.token, + token, ); return () => { client.disconnectUser(); }; - }, [client, config.userId, config.token]); + }, [client, token, userId]); const [activeCall, setActiveCall] = useState(); useEffect(() => { if (!client) return; let joinInterrupted = false; - const call = client.call(config.callType, config.callId); + const call = client.call(callType, callId); const currentCall = call.join().then(() => { if (!joinInterrupted) { setActiveCall(call); @@ -57,7 +66,7 @@ export const CompositeApp = () => { setActiveCall(undefined); }); }; - }, [client, config.callId, config.callType]); + }, [client, callType, callId]); if (!client) { return

Connecting...

; @@ -65,29 +74,23 @@ export const CompositeApp = () => { return ( - + {activeCall && ( - + + )} - + {/* */} + ); }; -const UiDispatcher = (props: { layout: LayoutId }) => { - const { layout } = props; - const { ParticipantsView, ScreenShareView } = - Layouts[layout || DEFAULT_LAYOUT_ID]; - - const { useHasOngoingScreenShare } = useCallStateHooks(); - const hasScreenShare = useHasOngoingScreenShare(); - if (hasScreenShare) { - return ; - } +const StreamThemeWrapper = ({ children }: PropsWithChildren) => { + // TODO: background style - return ; + return {children}; }; diff --git a/sample-apps/react/egress-composite/src/ConfigurationContext.tsx b/sample-apps/react/egress-composite/src/ConfigurationContext.tsx new file mode 100644 index 0000000000..5a5c811db4 --- /dev/null +++ b/sample-apps/react/egress-composite/src/ConfigurationContext.tsx @@ -0,0 +1,74 @@ +import { createContext, useContext } from 'react'; +import { decode } from 'js-base64'; + +export type ConfigurationValue = { + base_url?: string; + + api_key: string; + call_type: string; + call_id: string; + token: string; + user_id: string; // pulled from the token payload + + ext_css?: string; + + layout?: 'grid' | 'single_participant' | 'spotlight' | 'mobile'; + screenshare_layout?: 'single_participant' | 'spotlight'; + + options: { + 'video.background_color'?: string; + 'video.scale_mode'?: 'fill' | 'fit'; + 'video.screenshare_scale_mode'?: 'fill' | 'fit'; + + 'logo.image_url'?: string; + 'logo.horizontal_position'?: 'center' | 'left' | 'right'; + 'logo.vertical_position'?: 'center' | 'left' | 'right'; + + 'participant.label_display'?: boolean; + 'participant.label_text_color'?: string; + 'participant.label_background_color'?: string; + 'participant.label_display_border'?: boolean; + 'participant.label_border_radius'?: string; + 'participant.label_border_color'?: string; + 'participant.label_horizontal_position'?: 'center' | 'left' | 'right'; + 'participant.label_vertical_position'?: 'center' | 'left' | 'right'; + + // participant_border_color: string; + // participant_border_radius: string; + // participant_border_width: string; + 'participant.participant_highlight_border_color'?: string; // talking + 'participant.placeholder_background_color'?: string; + + // used with any layout + 'layout.size_percentage'?: number; + + // grid-specific + 'layout.grid.gap'?: string; + 'layout.grid.page_size'?: number; + // dominant_speaker-specific (single-participant) + 'layout.single_participant.mode'?: 'shuffle' | 'default'; + 'layout.single_participant.shuffle_delay'?: number; + // spotlight-specific + 'layout.spotlight.bar_position'?: 'top' | 'right' | 'bottom' | 'left'; + 'layout.spotlight.bar_limit'?: number; + }; +}; + +export const ConfigurationContext = createContext( + {} as ConfigurationValue, +); + +export const extractPayloadFromToken = (token: string) => { + const [, payload] = token.split('.'); + + if (!payload) throw new Error('Malformed token, missing payload'); + + try { + return (JSON.parse(decode(payload)) ?? {}) as Record; + } catch (e) { + console.log('Error parsing token payload', e); + return {}; + } +}; + +export const useConfigurationContext = () => useContext(ConfigurationContext); diff --git a/sample-apps/react/egress-composite/src/components/LogoAndTitleOverlay.tsx b/sample-apps/react/egress-composite/src/components/LogoAndTitleOverlay.tsx new file mode 100644 index 0000000000..378763fc7f --- /dev/null +++ b/sample-apps/react/egress-composite/src/components/LogoAndTitleOverlay.tsx @@ -0,0 +1,36 @@ +import { useConfigurationContext } from '../ConfigurationContext'; + +export const LogoAndTitleOverlay = () => { + const { options } = useConfigurationContext(); + + const image_url = options['logo.image_url']; + + return ( +
+ {/* {text?.length && ( +
+ {text} +
+ )} */} + {image_url && ( + logo + )} +
+ ); +}; diff --git a/sample-apps/react/egress-composite/src/components/UIDispatcher.tsx b/sample-apps/react/egress-composite/src/components/UIDispatcher.tsx new file mode 100644 index 0000000000..34677500c5 --- /dev/null +++ b/sample-apps/react/egress-composite/src/components/UIDispatcher.tsx @@ -0,0 +1,23 @@ +import { useCallStateHooks } from '@stream-io/video-react-sdk'; + +import { useConfigurationContext } from '../ConfigurationContext'; +import { LayoutType, layoutMap } from './layouts'; +import { Spotlight } from './layouts/Spotlight'; + +const DEFAULT_LAYOUT: LayoutType = 'spotlight'; +const DEFAULT_SCREENSHARE_LAYOUT: LayoutType = 'spotlight'; + +export const UIDispatcher = () => { + const { + layout = DEFAULT_LAYOUT, + screenshare_layout = DEFAULT_SCREENSHARE_LAYOUT, + } = useConfigurationContext(); + const { useHasOngoingScreenShare } = useCallStateHooks(); + const hasScreenShare = useHasOngoingScreenShare(); + + const DefaultView = layoutMap[layout]?.[0] ?? Spotlight; + + const ScreenShareView = layoutMap[screenshare_layout]?.[1] ?? Spotlight; + + return hasScreenShare ? : ; +}; diff --git a/sample-apps/react/egress-composite/src/components/index.ts b/sample-apps/react/egress-composite/src/components/index.ts new file mode 100644 index 0000000000..1b7f446e53 --- /dev/null +++ b/sample-apps/react/egress-composite/src/components/index.ts @@ -0,0 +1,2 @@ +export * from './LogoAndTitleOverlay'; +export * from './UIDispatcher'; diff --git a/sample-apps/react/egress-composite/src/layouts/dominant-speaker/AudioTracks.tsx b/sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/AudioTracks.tsx similarity index 92% rename from sample-apps/react/egress-composite/src/layouts/dominant-speaker/AudioTracks.tsx rename to sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/AudioTracks.tsx index 24bb9a9bc3..cd3a671770 100644 --- a/sample-apps/react/egress-composite/src/layouts/dominant-speaker/AudioTracks.tsx +++ b/sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/AudioTracks.tsx @@ -12,7 +12,7 @@ export const AudioTracks = (props: { key={participant.sessionId} audioStream={participant.audioStream} muted={participant.sessionId === dominantSpeaker?.sessionId} - data-userId={participant.userId} + data-user-id={participant.userId} /> ))} diff --git a/sample-apps/react/egress-composite/src/layouts/dominant-speaker/Spotlight.scss b/sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/DominantSpeaker.scss similarity index 85% rename from sample-apps/react/egress-composite/src/layouts/dominant-speaker/Spotlight.scss rename to sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/DominantSpeaker.scss index d619ccb290..97ed594294 100644 --- a/sample-apps/react/egress-composite/src/layouts/dominant-speaker/Spotlight.scss +++ b/sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/DominantSpeaker.scss @@ -1,4 +1,4 @@ -.spotlight-container { +.dominant-speaker__container { height: 100vh; .str-video__participant-view { diff --git a/sample-apps/react/egress-composite/src/layouts/dominant-speaker/DominantSpeaker.tsx b/sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/DominantSpeaker.tsx similarity index 93% rename from sample-apps/react/egress-composite/src/layouts/dominant-speaker/DominantSpeaker.tsx rename to sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/DominantSpeaker.tsx index 65d0f94835..e644087cec 100644 --- a/sample-apps/react/egress-composite/src/layouts/dominant-speaker/DominantSpeaker.tsx +++ b/sample-apps/react/egress-composite/src/components/layouts/DominantSpeaker/DominantSpeaker.tsx @@ -7,7 +7,7 @@ import { } from '@stream-io/video-react-sdk'; import { useSpotlightParticipant } from './useSpotlightParticipant'; import { useEgressReadyWhenAnyParticipantMounts } from '../egressReady'; -import './Spotlight.scss'; +import './DominantSpeaker.scss'; import { AudioTracks } from './AudioTracks'; export const DominantSpeaker = () => { @@ -24,7 +24,7 @@ export const DominantSpeaker = () => { if (!activeCall) return

No active call

; return ( <> -
+
{speakerInSpotlight && ( { @@ -26,9 +26,9 @@ export const DominantSpeakerScreenShare = () => { return ( <> -
+