Skip to content

Commit db5e0dd

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

File tree

13 files changed

+735
-106
lines changed

13 files changed

+735
-106
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/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ export const VideoTileGrid: React.FC<React.PropsWithChildren<Props>> = ({
4747
}) => {
4848
const { tileId: featureTileId } = useFeaturedTileState();
4949
const { tiles } = useRemoteVideoTileState();
50-
const { tileId: contentTileId } = useContentShareState();
50+
const { tiles: contentTiles } = useContentShareState();
5151
const { isVideoEnabled } = useLocalVideo();
52+
const hasContentShare = contentTiles.length > 0;
5253
const featured =
53-
(layout === 'featured' && !!featureTileId) || !!contentTileId;
54-
const remoteSize = tiles.length + (contentTileId ? 1 : 0);
54+
(layout === 'featured' && !!featureTileId) || hasContentShare;
55+
const remoteSize = tiles.length + contentTiles.length;
5556
const gridSize =
5657
remoteSize > 1 && isVideoEnabled ? remoteSize + 1 : remoteSize;
5758

src/providers/ContentShareProvider/docs/ContentShareProvider.mdx

Lines changed: 69 additions & 4 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,48 @@ 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 } = useContentShareState();
102+
103+
return (
104+
<div>
105+
<p>Content shares available: {tiles.length}</p>
106+
<p>Can start content share: {canStartContentShare ? 'Yes' : 'No'}</p>
107+
<div>
108+
{tiles.map(tileId => (
109+
<ContentShare
110+
key={tileId}
111+
tileId={tileId}
112+
nameplate={`Shared by: ${tileIdToAttendeeId[tileId.toString()]}`}
113+
/>
114+
))}
115+
</div>
116+
</div>
117+
);
118+
};
119+
```
120+
56121
## Usage without MeetingProvider
57122

58123
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.
@@ -63,7 +128,7 @@ import { ContentShareProvider } from 'amazon-chime-sdk-component-library-react';
63128

64129
const App = () => (
65130
<CustomAudioVideoProvider>
66-
<ContentShareProvider>
131+
<ContentShareProvider maxContentShares={2}>
67132
<MyComponent />
68133
</ContentShareProvider>
69134
</CustomAudioVideoProvider>

0 commit comments

Comments
 (0)