Skip to content

Commit

Permalink
feat(egress-composite): new configuration system (#689)
Browse files Browse the repository at this point in the history
Adds `ConfigurationContext` for configuration data provisioning across
the whole application.

To load the application append this script to the page `head`:
```html
<script type="application/javascript">
	window.StreamCompositeApp.configureAndRender({
                // required
		call_id: "<your-call-id>",
                // optional
		options: {
			layout: {
				main: "grid",
				screenshare: "dominant_speaker",
			},
		},
	});
</script>
```
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 <[email protected]>
Co-authored-by: Tommaso Barbugli <[email protected]>
  • Loading branch information
3 people authored Aug 24, 2023
1 parent 2e400e7 commit 7f41c71
Show file tree
Hide file tree
Showing 30 changed files with 337 additions and 158 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-react-sample-apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
2 changes: 1 addition & 1 deletion sample-apps/react/egress-composite/.env-example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VITE_STREAM_API_KEY="<your API key>"
VITE_STREAM_TOKEN="<user token>"
VITE_STREAM_USER_TOKEN="<user token>"
1 change: 1 addition & 0 deletions sample-apps/react/egress-composite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
Expand Down
2 changes: 1 addition & 1 deletion sample-apps/react/egress-composite/src/CompositeApp.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

.str-video {
color: var(--str-video__text-color1);
position: relative;
}

body {
Expand All @@ -24,7 +25,6 @@ body {
}

#root {
max-width: 1920px;
margin: 0 auto;
text-align: center;
}
57 changes: 30 additions & 27 deletions sample-apps/react/egress-composite/src/CompositeApp.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { PropsWithChildren, useEffect, useState } from 'react';
import {
Call,
CallingState,
Expand All @@ -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<StreamVideoClient>(
() => 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<Call>();
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);
Expand All @@ -57,37 +66,31 @@ export const CompositeApp = () => {
setActiveCall(undefined);
});
};
}, [client, config.callId, config.callType]);
}, [client, callType, callId]);

if (!client) {
return <h2>Connecting...</h2>;
}

return (
<StreamVideo client={client}>
<StreamTheme>
<StreamThemeWrapper>
<EgressReadyNotificationProvider>
{activeCall && (
<StreamCallProvider call={activeCall}>
<UiDispatcher layout={config.layout} />
<UIDispatcher />
<LogoAndTitleOverlay />
</StreamCallProvider>
)}
</EgressReadyNotificationProvider>
</StreamTheme>
{/* <StyleComponent /> */}
</StreamThemeWrapper>
</StreamVideo>
);
};

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 <ScreenShareView />;
}
const StreamThemeWrapper = ({ children }: PropsWithChildren) => {
// TODO: background style

return <ParticipantsView />;
return <StreamTheme>{children}</StreamTheme>;
};
74 changes: 74 additions & 0 deletions sample-apps/react/egress-composite/src/ConfigurationContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ConfigurationValue>(
{} 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<string, unknown>;
} catch (e) {
console.log('Error parsing token payload', e);
return {};
}
};

export const useConfigurationContext = () => useContext(ConfigurationContext);
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useConfigurationContext } from '../ConfigurationContext';

export const LogoAndTitleOverlay = () => {
const { options } = useConfigurationContext();

const image_url = options['logo.image_url'];

return (
<div
style={{
top: 0,
left: 0,
position: 'absolute',
width: '100%',
height: '100%',
}}
>
{/* {text?.length && (
<div
data-test-id="title"
style={{ ...DEFAULT_TITLE_STYLE, ...titleStyle }}
>
{text}
</div>
)} */}
{image_url && (
<img
data-test-id="logo"
src={image_url}
// style={{ ...DEFAULT_LOGO_STYLE, ...logoStyle }}
alt="logo"
/>
)}
</div>
);
};
23 changes: 23 additions & 0 deletions sample-apps/react/egress-composite/src/components/UIDispatcher.tsx
Original file line number Diff line number Diff line change
@@ -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 ? <ScreenShareView /> : <DefaultView />;
};
2 changes: 2 additions & 0 deletions sample-apps/react/egress-composite/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './LogoAndTitleOverlay';
export * from './UIDispatcher';
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.spotlight-container {
.dominant-speaker__container {
height: 100vh;

.str-video__participant-view {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -24,7 +24,7 @@ export const DominantSpeaker = () => {
if (!activeCall) return <h2>No active call</h2>;
return (
<>
<div className="spotlight-container">
<div className="dominant-speaker__container">
{speakerInSpotlight && (
<ParticipantView
participant={speakerInSpotlight}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
.screen-share-container {
.dominant-speaker-screen-share__container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;

.screen-share-player {
.dominant-speaker-screen-share__player {
flex: 1;
}

.current-speaker {
.dominant-speaker-screen-share__current-speaker {
position: absolute;
width: 240px;
right: 10px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Video,
} from '@stream-io/video-react-sdk';
import { useEgressReadyWhenAnyParticipantMounts } from '../egressReady';
import './ScreenShare.scss';
import './DominantSpeakerScreenShare.scss';
import { AudioTracks } from './AudioTracks';

export const DominantSpeakerScreenShare = () => {
Expand All @@ -26,9 +26,9 @@ export const DominantSpeakerScreenShare = () => {

return (
<>
<div className="screen-share-container">
<div className="dominant-speaker-screen-share__container">
<Video
className="screen-share-player"
className="dominant-speaker-screen-share__player"
participant={screenSharingParticipant}
kind="screen"
autoPlay
Expand All @@ -39,7 +39,7 @@ export const DominantSpeakerScreenShare = () => {
Presenter:{' '}
{screenSharingParticipant.name || screenSharingParticipant.userId}
</span>
<div className="current-speaker">
<div className="dominant-speaker-screen-share__current-speaker">
<ParticipantView
participant={screenSharingParticipant}
ParticipantViewUI={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DominantSpeaker } from './DominantSpeaker';
export { DominantSpeakerScreenShare } from './DominantSpeakerScreenShare';
Loading

0 comments on commit 7f41c71

Please sign in to comment.