diff --git a/packages/react-sdk/docusaurus/docs/React/03-ui-components/core/call-layout.mdx b/packages/react-sdk/docusaurus/docs/React/03-ui-components/core/call-layout.mdx index dcdd58a9db..1968c1fa83 100644 --- a/packages/react-sdk/docusaurus/docs/React/03-ui-components/core/call-layout.mdx +++ b/packages/react-sdk/docusaurus/docs/React/03-ui-components/core/call-layout.mdx @@ -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` @@ -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` | diff --git a/packages/react-sdk/docusaurus/docs/React/06-ui-cookbook/13-participant-view-customizations.mdx b/packages/react-sdk/docusaurus/docs/React/06-ui-cookbook/13-participant-view-customizations.mdx index 14b7ff4b09..b5781af1d8 100644 --- a/packages/react-sdk/docusaurus/docs/React/06-ui-cookbook/13-participant-view-customizations.mdx +++ b/packages/react-sdk/docusaurus/docs/React/06-ui-cookbook/13-participant-view-customizations.mdx @@ -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 + +// or + +``` + ## Standalone ParticipantView customization (in custom call layouts) The `ParticipantViewUI` property accepts three possible values: diff --git a/packages/react-sdk/src/core/components/CallLayout/LivestreamLayout.tsx b/packages/react-sdk/src/core/components/CallLayout/LivestreamLayout.tsx index 0c1b6331e8..82fe0ef3cd 100644 --- a/packages/react-sdk/src/core/components/CallLayout/LivestreamLayout.tsx +++ b/packages/react-sdk/src/core/components/CallLayout/LivestreamLayout.tsx @@ -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. */ @@ -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 /> )} diff --git a/packages/react-sdk/src/core/components/CallLayout/PaginatedGridLayout.tsx b/packages/react-sdk/src/core/components/CallLayout/PaginatedGridLayout.tsx index 3c46dc8e74..55893bc63c 100644 --- a/packages/react-sdk/src/core/components/CallLayout/PaginatedGridLayout.tsx +++ b/packages/react-sdk/src/core/components/CallLayout/PaginatedGridLayout.tsx @@ -20,11 +20,12 @@ type PaginatedGridLayoutGroupProps = { * The group of participants to render. */ group: Array; -} & Pick & +} & Pick & Required>; const PaginatedGridLayoutGroup = ({ group, + mirror, VideoPlaceholder, ParticipantViewUI, }: PaginatedGridLayoutGroupProps) => { @@ -43,6 +44,7 @@ const PaginatedGridLayoutGroup = ({ key={participant.sessionId} participant={participant} muteAudio + mirror={mirror} VideoPlaceholder={VideoPlaceholder} ParticipantViewUI={ParticipantViewUI} /> @@ -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 @@ -76,6 +84,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => { ? props.groupSize || GROUP_SIZE : GROUP_SIZE, excludeLocalParticipant = false, + mirrorLocalParticipantVideo = true, pageArrowsVisible = true, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, @@ -121,6 +130,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => { }, [page, pageCount]); const selectedGroup = participantGroups[page]; + const mirror = mirrorLocalParticipantVideo ? undefined : false; if (!call) return null; @@ -143,6 +153,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => { {selectedGroup && ( diff --git a/packages/react-sdk/src/core/components/CallLayout/SpeakerLayout.tsx b/packages/react-sdk/src/core/components/CallLayout/SpeakerLayout.tsx index f511837fb6..aa5d206237 100644 --- a/packages/react-sdk/src/core/components/CallLayout/SpeakerLayout.tsx +++ b/packages/react-sdk/src/core/components/CallLayout/SpeakerLayout.tsx @@ -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 @@ -58,6 +63,7 @@ export const SpeakerLayout = ({ VideoPlaceholder, participantsBarPosition = 'bottom', participantsBarLimit, + mirrorLocalParticipantVideo = true, excludeLocalParticipant = false, pageArrowsVisible = true, }: SpeakerLayoutProps) => { @@ -117,6 +123,8 @@ export const SpeakerLayout = ({ ); } + const mirror = mirrorLocalParticipantVideo ? undefined : false; + if (!call) return null; return ( @@ -134,6 +142,7 @@ export const SpeakerLayout = ({ @@ -177,6 +187,7 @@ export const SpeakerLayout = ({ participant={participant} ParticipantViewUI={ParticipantViewUIBar} VideoPlaceholder={VideoPlaceholder} + mirror={mirror} muteAudio={true} /> diff --git a/packages/react-sdk/src/core/components/ParticipantView/ParticipantView.tsx b/packages/react-sdk/src/core/components/ParticipantView/ParticipantView.tsx index 62b6f4c85b..a444a616d5 100644 --- a/packages/react-sdk/src/core/components/ParticipantView/ParticipantView.tsx +++ b/packages/react-sdk/src/core/components/ParticipantView/ParticipantView.tsx @@ -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. @@ -69,6 +75,7 @@ export const ParticipantView = forwardRef( { participant, trackType = 'videoTrack', + mirror, muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, @@ -176,6 +183,7 @@ export const ParticipantView = forwardRef( trackType !== 'videoTrack' || isParticipantVideoEnabled(participant.sessionId) } + mirror={mirror} autoPlay /> {isComponentType(ParticipantViewUI) ? ( diff --git a/packages/react-sdk/src/core/components/Video/Video.tsx b/packages/react-sdk/src/core/components/Video/Video.tsx index 29ded2462e..809c3b6d63 100644 --- a/packages/react-sdk/src/core/components/Video/Video.tsx +++ b/packages/react-sdk/src/core/components/Video/Video.tsx @@ -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. */ @@ -61,6 +65,7 @@ export type VideoProps = ComponentPropsWithoutRef<'video'> & { export const Video = ({ enabled, + mirror, trackType, participant, className, @@ -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 ( <>