Skip to content

Commit ba943b9

Browse files
committed
Add support for multiple content shares
1 parent efb7f0f commit ba943b9

File tree

14 files changed

+786
-109
lines changed

14 files changed

+786
-109
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- Added support for multiple content shares in meetings through the `ContentShareProvider`. The provider now accepts a `maxContentShares` prop (default: 1, range 1-2) to specify the maximum number of concurrent content shares allowed.
15+
- Added new collections in `ContentShareState` to track multiple content shares: `tiles`, `tileIdToAttendeeId`, and `attendeeIdToTileId`.
16+
- Added optional `tileId` prop to the `ContentShare` component to specify which content share to render.
17+
- Added `canStartContentShare` state to control when content sharing is allowed based on the current number of shares and configured maximum.
18+
- Maintained backward compatibility by keeping `tileId` and `sharingAttendeeId` properties, which now point to the most recently started content share when multiple shares are present.
19+
1420
### Removed
1521

1622
### Changed

src/components/sdk/ContentShare/ContentShare.mdx

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import { ContentShare } from './';
55

66
# ContentShare
77

8-
The `ContentShare` component renders a `ContentTile` for the active content share video, remote or local.
8+
The `ContentShare` component renders a `ContentTile` for a content share video, remote or local.
99

1010
If used within the `VideoGrid` component, it will automatically place the active tile in the featured grid slot. It takes precedence over the featured video tile.
1111

1212
Once a meeting session has been started, a user can start and stop content sharing by using the `useContentShareControls` hook.
1313

14+
## Multiple Content Shares
15+
16+
With the support for multiple content shares, you can now specify which content share tile to render by providing the `tileId` prop. If no `tileId` is provided, the component will render the default content share tile from (`const { tileId } = useContentShareState()`).
17+
1418
## Importing
1519

1620
```javascript
@@ -19,26 +23,85 @@ import { ContentShare } from 'amazon-chime-sdk-component-library-react';
1923

2024
## Usage
2125

26+
### With Single Content Share
27+
2228
```jsx
2329
import React from 'react';
2430
import {
2531
MeetingProvider,
2632
ContentShare,
27-
useContentShareControls
33+
useContentShareControls,
2834
} from 'amazon-chime-sdk-component-library-react';
2935

3036
const App = () => {
3137
const { toggleContentShare } = useContentShareControls();
3238

3339
return (
3440
<MeetingProvider>
35-
<ContentShare nameplate='Content share' />
41+
<ContentShare nameplate="Content share" />
3642
<button onClick={toggleContentShare}>Toggle content share</button>
3743
</MeetingProvider>
3844
);
3945
};
4046
```
4147

48+
### With Multiple Content Shares
49+
50+
```jsx
51+
import React from 'react';
52+
import {
53+
MeetingProvider,
54+
ContentShare,
55+
ContentShareProvider,
56+
useContentShareState,
57+
useContentShareControls,
58+
} from 'amazon-chime-sdk-component-library-react';
59+
60+
const App = () => {
61+
return (
62+
<MeetingProvider>
63+
<ContentShareProvider maxContentShares={2}>
64+
<ContentShareView />
65+
<ContentShareControls />
66+
</ContentShareProvider>
67+
</MeetingProvider>
68+
);
69+
};
70+
71+
const ContentShareView = () => {
72+
const { tiles, tileIdToAttendeeId } = useContentShareState();
73+
74+
return (
75+
<div>
76+
{tiles.length === 0 ? (
77+
<p>No content is being shared</p>
78+
) : (
79+
<div className="content-share-container">
80+
{tiles.map((tileId) => (
81+
<ContentShare
82+
key={tileId}
83+
tileId={tileId}
84+
nameplate={`Shared by: ${tileIdToAttendeeId[tileId.toString()]}`}
85+
/>
86+
))}
87+
</div>
88+
)}
89+
</div>
90+
);
91+
};
92+
93+
const ContentShareControls = () => {
94+
const { toggleContentShare } = useContentShareControls();
95+
const { canStartContentShare } = useContentShareState();
96+
97+
return (
98+
<button onClick={toggleContentShare} disabled={!canStartContentShare}>
99+
Share content
100+
</button>
101+
);
102+
};
103+
```
104+
42105
## Props
43106

44107
<ArgTypes of={ContentShare} />

src/components/sdk/ContentShare/index.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,49 @@ import { BaseSdkProps } from '../Base';
1010

1111
interface Props extends BaseSdkProps {
1212
nameplate?: string;
13+
tileId?: number;
1314
}
1415

1516
export const ContentShare: React.FC<React.PropsWithChildren<Props>> = ({
1617
className,
18+
tileId,
1719
...rest
1820
}) => {
1921
const audioVideo = useAudioVideo();
20-
const { tileId } = useContentShareState();
22+
const contentShareState = useContentShareState();
23+
24+
// Use the provided tileId or fall back to the default (for backward compatibility)
25+
const tileIdToRender =
26+
tileId !== undefined ? tileId : contentShareState.tileId;
27+
28+
const attendeeId = tileIdToRender
29+
? contentShareState.tileIdToAttendeeId[tileIdToRender.toString()]
30+
: null;
31+
2132
const videoEl = useRef<HTMLVideoElement | null>(null);
2233

2334
useEffect(() => {
24-
if (!audioVideo || !videoEl.current || !tileId) {
35+
if (!audioVideo || !videoEl.current || !tileIdToRender) {
2536
return;
2637
}
2738

28-
audioVideo.bindVideoElement(tileId, videoEl.current);
39+
audioVideo.bindVideoElement(tileIdToRender, videoEl.current);
2940

3041
return () => {
31-
const tile = audioVideo.getVideoTile(tileId);
42+
const tile = audioVideo.getVideoTile(tileIdToRender);
3243
if (tile) {
33-
audioVideo.unbindVideoElement(tileId);
44+
audioVideo.unbindVideoElement(tileIdToRender);
3445
}
3546
};
36-
}, [audioVideo, tileId]);
47+
}, [audioVideo, tileIdToRender]);
3748

38-
return tileId ? (
49+
return tileIdToRender ? (
3950
<ContentTile
4051
objectFit="contain"
41-
className={className || ''}
52+
className={`ch-content-share--${tileIdToRender} ${className || ''}`}
4253
{...rest}
4354
ref={videoEl}
55+
data-content-share-attendee={attendeeId}
4456
/>
4557
) : null;
4658
};

src/components/sdk/FeaturedRemoteVideos/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,20 @@ export const FeaturedRemoteVideos: FC<React.PropsWithChildren<Props>> = (
2121
const gridData = useGridData();
2222
const { roster } = useRosterState();
2323
const { tileId: featuredTileId } = useFeaturedTileState();
24-
const { tileId: contentTileId } = useContentShareState();
24+
const { tiles: contentTiles } = useContentShareState();
2525
const { tiles, tileIdToAttendeeId } = useRemoteVideoTileState();
2626

2727
return (
2828
<>
2929
{tiles.map((tileId) => {
30-
const featured = !contentTileId && featuredTileId === tileId;
30+
const hasContentShare = contentTiles.length > 0;
31+
const featured = !hasContentShare && featuredTileId === tileId;
3132
const styles = gridData && featured ? 'grid-area: ft;' : '';
3233
const classes = `${featured ? 'ch-featured-tile' : ''} ${
3334
props.className || ''
3435
}`;
3536
const attendee = roster[tileIdToAttendeeId[tileId]] || {};
36-
const { name }: any = attendee;
37+
const { name = undefined }: { name?: string } = attendee;
3738

3839
return (
3940
<RemoteVideo

src/components/sdk/VideoTileGrid/VideoTileGrid.mdx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { VideoTileGrid } from './';
55

66
# VideoTileGrid
77

8-
The `VideoTileGrid` component renders all meeting session video tiles in a responsive grid layout. This includes the local tile, remote tiles, and content share tile. By default a user joins without video, so in order to see the VideoTileGrid there must be at least one video tile being shared. To start sharing a video, see the [LocalVideo](?path=/docs/sdk-components-localvideo--page) component.
8+
The `VideoTileGrid` component renders all meeting session video tiles in a responsive grid layout. This includes the local tile, remote tiles, and content share tile. By default a user joins without video, so in order to see the VideoTileGrid there must be at least one video tile being shared. To start sharing a video, see the [LocalVideo](?path=/docs/sdk-components-localvideo--page) component.
99

1010
## Importing
1111

@@ -19,7 +19,7 @@ import { VideoTileGrid } from 'amazon-chime-sdk-component-library-react';
1919
import React from 'react';
2020
import {
2121
MeetingProvider,
22-
VideoTileGrid
22+
VideoTileGrid,
2323
} from 'amazon-chime-sdk-component-library-react';
2424

2525
const App = () => (
@@ -29,6 +29,44 @@ const App = () => (
2929
);
3030
```
3131

32+
## Content Share Behavior
33+
34+
The `VideoTileGrid` component does not support displaying multiple content shares simultaneously:
35+
36+
- It can display only one content share tile at a time in the featured area
37+
- When multiple content shares are active, only the most recently started content share is displayed, and earlier shares are ignored
38+
- The grid size calculation always allocates exactly one tile space for content sharing (when content is present), regardless of how many content share tiles actually exist
39+
40+
This means that even if there are multiple content share tiles available through `useContentShareState()`, the VideoTileGrid will only render one of them.
41+
42+
### Custom Handling of Multiple Content Shares
43+
44+
If you need to support multiple simultaneous content shares, you'll need to implement a custom grid using `useContentShareState` hook and `ContentShare` sdk component.
45+
46+
```jsx
47+
import React from 'react';
48+
import {
49+
useContentShareState,
50+
ContentShare,
51+
} from 'amazon-chime-sdk-component-library-react';
52+
53+
const CustomMultiContentShareGrid = () => {
54+
// Access all content share tiles
55+
const { tiles: contentTiles } = useContentShareState();
56+
57+
return (
58+
<div className="custom-grid">
59+
{/* Render each content share tile */}
60+
{contentTiles.map((tileId) => (
61+
<ContentShare key={tileId} tileId={tileId} />
62+
))}
63+
64+
{/* Other video tiles */}
65+
</div>
66+
);
67+
};
68+
```
69+
3270
## Props
3371

3472
<ArgTypes of={VideoTileGrid} />

src/components/sdk/VideoTileGrid/index.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,21 @@ export const VideoTileGrid: React.FC<React.PropsWithChildren<Props>> = ({
4646
...rest
4747
}) => {
4848
const { tileId: featureTileId } = useFeaturedTileState();
49-
const { tiles } = useRemoteVideoTileState();
50-
const { tileId: contentTileId } = useContentShareState();
49+
const { tiles: remoteVideoTiles } = useRemoteVideoTileState();
50+
const { tiles: contentShareTiles } = useContentShareState();
5151
const { isVideoEnabled } = useLocalVideo();
52-
const featured =
53-
(layout === 'featured' && !!featureTileId) || !!contentTileId;
54-
const remoteSize = tiles.length + (contentTileId ? 1 : 0);
52+
53+
const hasContentShare = contentShareTiles.length > 0;
54+
// contentShareSize is counted as 1 in the grid size calculation, since only the most
55+
// recent content share tile is displayed in the featured area when multiple content shares exist
56+
const contentShareSize = hasContentShare ? 1 : 0;
57+
const remoteSize = remoteVideoTiles.length + contentShareSize;
5558
const gridSize =
5659
remoteSize > 1 && isVideoEnabled ? remoteSize + 1 : remoteSize;
5760

61+
const featured =
62+
(layout === 'featured' && !!featureTileId) || hasContentShare;
63+
5864
return (
5965
<VideoGrid {...rest} size={gridSize} layout={featured ? 'featured' : null}>
6066
<ContentShare css="grid-area: ft;" />

src/providers/ContentShareProvider/docs/ContentShareProvider.mdx

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ import { Meta } from '@storybook/blocks';
66

77
The `ContentShareProvider` provides state and functionality for content sharing.
88

9+
### Props
10+
11+
```javascript
12+
{
13+
// Maximum number of concurrent content shares allowed (default: 1, range: 1-2)
14+
maxContentShares?: number;
15+
}
16+
```
17+
918
### State
1019

1120
```javascript
1221
{
13-
// The tile ID of the active content share
22+
// The tile ID of the active content share (deprecated, maintained for backward compatibility)
23+
// When multiple content shares are present, this points to the most recently started content share
1424
tileId: number | null;
1525

26+
// The chime attendee ID of the user sharing (deprecated, maintained for backward compatibility)
27+
// When multiple content shares are present, this points to the attendee of the most recently started content share
28+
sharingAttendeeId: string | null;
29+
1630
// Whether the content share is paused
1731
paused: boolean;
1832

@@ -22,8 +36,17 @@ The `ContentShareProvider` provides state and functionality for content sharing.
2236
// Whether or not the local user's content share is loading
2337
isLocalShareLoading: boolean;
2438

25-
// The chime attendee ID of the user sharing
26-
sharingAttendeeId: string | null;
39+
// Whether the user can start a content share based on current limits and `isLocalUserSharing`
40+
canStartContentShare: boolean;
41+
42+
// Array of all content share tile IDs
43+
tiles: number[];
44+
45+
// Map of tile IDs to attendee IDs
46+
tileIdToAttendeeId: { [key: string]: string };
47+
48+
// Map of attendee IDs to tile IDs
49+
attendeeIdToTileId: { [key: string]: number };
2750
}
2851
```
2952

@@ -53,6 +76,49 @@ const MyChild = () => {
5376
};
5477
```
5578

79+
## Multiple Content Shares
80+
81+
You can configure the maximum number of concurrent content shares allowed (up to 2):
82+
83+
```jsx
84+
import React from 'react';
85+
import {
86+
MeetingProvider,
87+
ContentShareProvider,
88+
useContentShareState,
89+
} from 'amazon-chime-sdk-component-library-react';
90+
91+
const App = () => (
92+
<MeetingProvider>
93+
{/* Override the default maxContentShares value (1) in ContentShareProvider */}
94+
<ContentShareProvider maxContentShares={2}>
95+
<MyComponent />
96+
</ContentShareProvider>
97+
</MeetingProvider>
98+
);
99+
100+
const MyComponent = () => {
101+
const { tiles, tileIdToAttendeeId, canStartContentShare } =
102+
useContentShareState();
103+
104+
return (
105+
<div>
106+
<p>Content shares available: {tiles.length}</p>
107+
<p>Can start content share: {canStartContentShare ? 'Yes' : 'No'}</p>
108+
<div>
109+
{tiles.map((tileId) => (
110+
<ContentShare
111+
key={tileId}
112+
tileId={tileId}
113+
nameplate={`Shared by: ${tileIdToAttendeeId[tileId.toString()]}`}
114+
/>
115+
))}
116+
</div>
117+
</div>
118+
);
119+
};
120+
```
121+
56122
## Usage without MeetingProvider
57123

58124
If you opt out of using `MeetingProvider`, you can drop in a `ContentShareProvider` and use its state. Make sure that its dependencies are rendered higher in the tree.

0 commit comments

Comments
 (0)