Skip to content

Commit

Permalink
feat: add a prop to control mirroring of local participant video (#1506)
Browse files Browse the repository at this point in the history
We get a lot of questions about disabling mirroring of local participant
video. It's currently possible to do by overriding the
`str-video__video--mirror` CSS class, but it seems that things will be
easier if we have a prop to control this behavior.

This PR adds an optional `mirrorLocalParticipantVideo` prop to all
built-in layout components. Setting it to `false` disable local
participant video mirroring.
  • Loading branch information
myandrienko authored Oct 3, 2024
1 parent a2cd09c commit ca12dc3
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,28 @@ const MyApp = () => {

#### Props

| Name | Description | Type |
| ------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `groupSize` | The number of participants to display per page | `number` \| `undefined` |
| `excludeLocalParticipant` | Whether to exclude the local participant from the grid | `boolean` \| `undefined` |
| `pageArrowsVisible` | Turns on/off the pagination arrows | `boolean` \| `undefined` |
| `ParticipantViewUI` | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) |
| `VideoPlaceholder` | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) |
| Name | Description | Type |
| ----------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `groupSize` | The number of participants to display per page | `number` \| `undefined` |
| `excludeLocalParticipant` | Whether to exclude the local participant from the grid | `boolean` \| `undefined` |
| `mirrorLocalParticipantVideo` | Whether to mirror the user's own video (default `true`) | `boolean` \| `undefined` |
| `pageArrowsVisible` | Turns on/off the pagination arrows | `boolean` \| `undefined` |
| `ParticipantViewUI` | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) |
| `VideoPlaceholder` | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) |

### `SpeakerLayout`

#### Props

| Name | Description | Type |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `participantsBarPosition` | The position of the participants who are not in focus, the default is `bottom`. Providing `null` will hide the bar | `top` \| `bottom` \| `left` \| `right` \| `null` |
| `excludeLocalParticipant` | Whether to exclude the local participant from the layout | `boolean` \| `undefined` |
| `pageArrowsVisible` | Turns on/off the pagination arrows | `boolean` \| `undefined` |
| `ParticipantViewUISpotlight` | The participant UI for the spotlight view, [see `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) |
| `ParticipantViewUIBar` | The participant UI for the participants in the bar, [see `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) |
| `VideoPlaceholder` | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) |
| Name | Description | Type |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `participantsBarPosition` | The position of the participants who are not in focus, the default is `bottom`. Providing `null` will hide the bar | `top` \| `bottom` \| `left` \| `right` \| `null` |
| `excludeLocalParticipant` | Whether to exclude the local participant from the layout | `boolean` \| `undefined` |
| `mirrorLocalParticipantVideo` | Whether to mirror the user's own video (default `true`) | `boolean` \| `undefined` |
| `pageArrowsVisible` | Turns on/off the pagination arrows | `boolean` \| `undefined` |
| `ParticipantViewUISpotlight` | The participant UI for the spotlight view, [see `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) |
| `ParticipantViewUIBar` | The participant UI for the participants in the bar, [see `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#participantviewui) |
| `VideoPlaceholder` | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) | [See `ParticipantView` documentation](../../../ui-components/core/participant-view/#videoplaceholder) |

### `LivestreamLayout`

Expand All @@ -110,6 +112,7 @@ const MyApp = () => {
| `showDuration` | Will show the duration of the livestream | `boolean` |
| `showLiveBadge` | Will show a badge whether the livestream is live or not | `boolean` |
| `showSpeakerName` | Will show the name of the speaker | `boolean` |
| `mirrorLocalParticipantVideo` | Whether to mirror the user's own video (default `true`) | `boolean` |
| `floatingParticipantProps` | Props to pass to the floating participant view | `object` |
| `floatingParticipantProps.position` | Position of the floating participant view | `string` |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ const App = ({ client, callId }) => {
};
```

### Non-mirrored video

By default, local participant video (video feed from the user's own camera) is mirrored, since people are generally more accustomed to seeing themselves that way. It's possible to disable this behavior using the `mirrorLocalParticipantVideo` prop, which is supported by all built-in layout components:

```tsx
<SpeakerLayout mirrorLocalParticipantVideo={false} />
// or
<PaginatedGridLayout mirrorLocalParticipantVideo={false} />
```

## Standalone ParticipantView customization (in custom call layouts)

The `ParticipantViewUI` property accepts three possible values:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ export type LivestreamLayoutProps = {
*/
showSpeakerName?: boolean;

/**
* When set to `false` disables mirroring of the local partipant's video.
* @default true
*/
mirrorLocalParticipantVideo?: boolean;

/**
* The props to pass to the floating participant element.
*/
Expand Down Expand Up @@ -116,6 +122,9 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
)}
participant={currentSpeaker}
ParticipantViewUI={FloatingParticipantOverlay || Overlay}
mirror={
props.mirrorLocalParticipantVideo !== false ? undefined : false
}
muteAudio // audio is rendered by ParticipantsAudio
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ type PaginatedGridLayoutGroupProps = {
* The group of participants to render.
*/
group: Array<StreamVideoParticipant>;
} & Pick<ParticipantViewProps, 'VideoPlaceholder'> &
} & Pick<ParticipantViewProps, 'VideoPlaceholder' | 'mirror'> &
Required<Pick<ParticipantViewProps, 'ParticipantViewUI'>>;

const PaginatedGridLayoutGroup = ({
group,
mirror,
VideoPlaceholder,
ParticipantViewUI,
}: PaginatedGridLayoutGroupProps) => {
Expand All @@ -43,6 +44,7 @@ const PaginatedGridLayoutGroup = ({
key={participant.sessionId}
participant={participant}
muteAudio
mirror={mirror}
VideoPlaceholder={VideoPlaceholder}
ParticipantViewUI={ParticipantViewUI}
/>
Expand All @@ -63,6 +65,12 @@ export type PaginatedGridLayoutProps = {
*/
excludeLocalParticipant?: boolean;

/**
* When set to `false` disables mirroring of the local partipant's video.
* @default true
*/
mirrorLocalParticipantVideo?: boolean;

/**
* Turns on/off the pagination arrows.
* @default true
Expand All @@ -76,6 +84,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
? props.groupSize || GROUP_SIZE
: GROUP_SIZE,
excludeLocalParticipant = false,
mirrorLocalParticipantVideo = true,
pageArrowsVisible = true,
VideoPlaceholder,
ParticipantViewUI = DefaultParticipantViewUI,
Expand Down Expand Up @@ -121,6 +130,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
}, [page, pageCount]);

const selectedGroup = participantGroups[page];
const mirror = mirrorLocalParticipantVideo ? undefined : false;

if (!call) return null;

Expand All @@ -143,6 +153,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
{selectedGroup && (
<PaginatedGridLayoutGroup
group={selectedGroup}
mirror={mirror}
VideoPlaceholder={VideoPlaceholder}
ParticipantViewUI={ParticipantViewUI}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export type SpeakerLayoutProps = {
* @default false
*/
excludeLocalParticipant?: boolean;
/**
* When set to `false` disables mirroring of the local partipant's video.
* @default true
*/
mirrorLocalParticipantVideo?: boolean;
/**
* Turns on/off the pagination arrows.
* @default true
Expand All @@ -58,6 +63,7 @@ export const SpeakerLayout = ({
VideoPlaceholder,
participantsBarPosition = 'bottom',
participantsBarLimit,
mirrorLocalParticipantVideo = true,
excludeLocalParticipant = false,
pageArrowsVisible = true,
}: SpeakerLayoutProps) => {
Expand Down Expand Up @@ -117,6 +123,8 @@ export const SpeakerLayout = ({
);
}

const mirror = mirrorLocalParticipantVideo ? undefined : false;

if (!call) return null;

return (
Expand All @@ -134,6 +142,7 @@ export const SpeakerLayout = ({
<ParticipantView
participant={participantInSpotlight}
muteAudio={true}
mirror={mirror}
trackType={
isSpeakerScreenSharing ? 'screenShareTrack' : 'videoTrack'
}
Expand Down Expand Up @@ -164,6 +173,7 @@ export const SpeakerLayout = ({
participant={participantInSpotlight}
ParticipantViewUI={ParticipantViewUIBar}
VideoPlaceholder={VideoPlaceholder}
mirror={mirror}
muteAudio={true}
/>
</div>
Expand All @@ -177,6 +187,7 @@ export const SpeakerLayout = ({
participant={participant}
ParticipantViewUI={ParticipantViewUIBar}
VideoPlaceholder={VideoPlaceholder}
mirror={mirror}
muteAudio={true}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export type ParticipantViewProps = {
*/
trackType?: VideoTrackType | 'none';

/**
* Forces participant's video to be mirrored or unmirrored. By default, video track
* from the local participant is mirrored, and all other videos are not mirrored.
*/
mirror?: boolean;

/**
* This prop is only useful for advanced use-cases (for example, building your own layout).
* When set to `true` it will mute the give participant's audio stream on the client side.
Expand Down Expand Up @@ -69,6 +75,7 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
{
participant,
trackType = 'videoTrack',
mirror,
muteAudio,
refs: { setVideoElement, setVideoPlaceholderElement } = {},
className,
Expand Down Expand Up @@ -176,6 +183,7 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
trackType !== 'videoTrack' ||
isParticipantVideoEnabled(participant.sessionId)
}
mirror={mirror}
autoPlay
/>
{isComponentType(ParticipantViewUI) ? (
Expand Down
12 changes: 10 additions & 2 deletions packages/react-sdk/src/core/components/Video/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & {
* even if the participant has published video.
*/
enabled?: boolean;

/**
* Forces the video to be mirrored or unmirrored. By default, video track
* from the local participant is mirrored, and all other videos are not mirrored.
*/
mirror?: boolean;
/**
* The track type to display.
*/
Expand Down Expand Up @@ -61,6 +65,7 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & {

export const Video = ({
enabled,
mirror,
trackType,
participant,
className,
Expand Down Expand Up @@ -147,7 +152,10 @@ export const Video = ({
viewportVisibilityState?.[trackType] === VisibilityState.INVISIBLE;

const hasNoVideoOrInvisible = !enabled || !isPublishingTrack || isInvisible;
const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
const mirrorVideo =
mirror === undefined
? isLocalParticipant && trackType === 'videoTrack'
: mirror;
const isScreenShareTrack = trackType === 'screenShareTrack';
return (
<>
Expand Down

0 comments on commit ca12dc3

Please sign in to comment.