Voice call Pip circular media view

This commit is contained in:
Timo K
2026-07-06 14:57:25 +02:00
parent ad76969ce2
commit 90f95c5bf1
4 changed files with 62 additions and 2 deletions

View File

@@ -238,7 +238,7 @@ export const InCallView: FC<InCallViewProps> = ({
// Merge the refs so they can attach to the same element
const containerRef = useMergedRefs(containerRef1, containerRef2);
const { showControls, header: headerStyle } = useUrlParams();
const { showControls, header: headerStyle, callIntent } = useUrlParams();
const muteAllAudio = useBehavior(muteAllAudio$);
const toggleAudio = useBehavior(muteStates.audio.toggle$);
@@ -503,6 +503,8 @@ export const InCallView: FC<InCallViewProps> = ({
showRingingStatus={vm.ringingStatusLocation === "tile"}
focusable={!contentObscured}
aria-hidden={contentObscured}
// Use the Circular rendering of the audio tile in PiP
circularAudioTile={callIntent === "audio"}
/>
);
}

View File

@@ -62,6 +62,42 @@ Please see LICENSE in the repository root for full details.
opacity: 50%;
}
/* When no video is shown, circular media has no tile box at all: just the
avatar rendered on the app background */
.media.circular .bg {
background-color: transparent;
}
/* A disc behind the avatar, poking out around its edge to form a circular
speaking indicator. It uses the same gradient as the speaking border of grid
tiles. */
.media.circular .bg::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
background:
linear-gradient(
119deg,
rgba(13, 92, 189, 0.7) 0%,
rgba(13, 189, 168, 0.7) 100%
),
linear-gradient(
180deg,
rgba(13, 92, 189, 0.9) 0%,
rgba(13, 189, 168, 0.9) 100%
);
background-blend-mode: overlay, normal;
opacity: 0; /* Hidden unless speaking */
transition: opacity ease 0.15s;
}
.media.circular.speaking .bg::before {
opacity: 1;
}
/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cqmin units */
@container mediaView (width > 0) {
@@ -70,6 +106,13 @@ unconditionally select the container so we can use cqmin units */
inline-size: 50cqmin;
block-size: 50cqmin;
}
.media.circular .bg::before {
/* Sized so that the disc extends beyond the avatar by the width of the
speaking indicator */
inline-size: calc(50cqmin + 2 * var(--cpd-border-width-4));
block-size: calc(50cqmin + 2 * var(--cpd-border-width-4));
}
}
.avatar > img {

View File

@@ -54,6 +54,9 @@ interface Props extends ComponentProps<typeof animated.div> {
rtcBackendIdentity?: string;
// The focus url, mainly for debugging purposes
focusUrl?: string;
circularAudioTile?: boolean;
// render a circular speaking indicator if circularAudioTile is enabled.
circularSpeakingIndicator?: boolean;
}
export const MediaView: FC<Props> = ({
@@ -84,6 +87,8 @@ export const MediaView: FC<Props> = ({
videoStreamStats,
rtcBackendIdentity,
focusUrl,
circularAudioTile,
circularSpeakingIndicator,
...props
}) => {
const { t } = useTranslation();
@@ -112,7 +117,9 @@ export const MediaView: FC<Props> = ({
return (
<animated.div
className={classNames(styles.media, className, {
[styles.circular]: circularAudioTile && !videoEnabled,
[styles.mirror]: mirror,
[styles.speaking]: circularSpeakingIndicator,
})}
style={style}
ref={ref}

View File

@@ -66,6 +66,7 @@ interface SpotlightItemBaseProps {
mxcAvatarUrl: string | undefined;
showNameTags: boolean;
focusable: boolean;
circularAudioTile?: boolean;
"aria-hidden"?: boolean;
}
@@ -103,8 +104,9 @@ const SpotlightRemoteUserMediaItem: FC<SpotlightRemoteUserMediaItemProps> = ({
...props
}) => {
const waitingForMedia = useBehavior(vm.waitingForMedia$);
const speaking = useBehavior(vm.speaking$);
return (
<MediaView waitingForMedia={waitingForMedia} mirror={false} {...props} />
<MediaView waitingForMedia={waitingForMedia} mirror={false} {...props} circularSpeakingIndicator={speaking} />
);
};
@@ -243,6 +245,7 @@ interface SpotlightItemProps {
targetHeight: number;
showNameTags: boolean;
showRingingStatus: boolean;
circularAudioTile?: boolean;
focusable: boolean;
intersectionObserver$: Observable<IntersectionObserver>;
/**
@@ -259,6 +262,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
targetHeight,
showNameTags,
showRingingStatus,
circularAudioTile,
focusable,
intersectionObserver$,
snap,
@@ -295,6 +299,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
displayName,
mxcAvatarUrl,
showNameTags,
circularAudioTile,
focusable,
"aria-hidden": ariaHidden,
};
@@ -389,6 +394,7 @@ interface Props {
showIndicators: boolean;
showNameTags: boolean;
showRingingStatus: boolean;
circularAudioTile?: boolean;
focusable: boolean;
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
@@ -404,6 +410,7 @@ export const SpotlightTile: FC<Props> = ({
showIndicators,
showNameTags,
showRingingStatus,
circularAudioTile,
focusable = true,
className,
style,
@@ -517,6 +524,7 @@ export const SpotlightTile: FC<Props> = ({
showRingingStatus={showRingingStatus}
showNameTags={showNameTags}
focusable={focusable}
circularAudioTile={circularAudioTile}
intersectionObserver$={intersectionObserver$}
// This is how we get the container to scroll to the right media
// when the previous/next buttons are clicked: we temporarily