snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
47
react/features/pip/components/web/DocumentPiPContent.tsx
Normal file
47
react/features/pip/components/web/DocumentPiPContent.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import createCache from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import GlobalStyles from '../../../base/ui/components/GlobalStyles.web';
|
||||
|
||||
import { DocumentPiPView } from './DocumentPiPView';
|
||||
|
||||
/**
|
||||
* Inner component for the Document PiP.
|
||||
*
|
||||
* @returns {React.ReactElement | null}
|
||||
*/
|
||||
export function DocumentPiPContent() {
|
||||
const pipWindow = useSelector((state: IReduxState) => state['features/pip'].pipWindow);
|
||||
const pipCache = useMemo(() => {
|
||||
if (!pipWindow || pipWindow.closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createCache({
|
||||
key: 'jitsi-pip',
|
||||
container: pipWindow.document.head
|
||||
});
|
||||
}, [ pipWindow ]);
|
||||
|
||||
useEffect(() => () => {
|
||||
pipCache?.sheet.flush();
|
||||
}, [ pipCache ]);
|
||||
|
||||
const pipRoot = pipWindow?.document.getElementById('pip-root');
|
||||
|
||||
if (!pipCache || !pipRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<CacheProvider value = { pipCache }>
|
||||
<GlobalStyles />
|
||||
<DocumentPiPView />
|
||||
</CacheProvider>,
|
||||
pipRoot
|
||||
);
|
||||
}
|
||||
180
react/features/pip/components/web/DocumentPiPView.tsx
Normal file
180
react/features/pip/components/web/DocumentPiPView.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../../../base/participants/functions';
|
||||
import DisplayNameBadge from '../../../display-name/components/web/DisplayNameBadge';
|
||||
import { getStageParticipantTypography } from '../../../display-name/components/web/styles';
|
||||
import { getLargeVideoParticipant } from '../../../large-video/functions';
|
||||
import { isPrejoinPageVisible } from '../../../prejoin/functions.any';
|
||||
import HangupButton from '../../../toolbox/components/HangupButton';
|
||||
import AudioMuteButton from '../../../toolbox/components/web/AudioMuteButton';
|
||||
import VideoMuteButton from '../../../toolbox/components/web/VideoMuteButton';
|
||||
import { getPiPVideoTrack, shouldShowPiPAvatar } from '../../functions';
|
||||
import logger from '../../logger';
|
||||
|
||||
const useStyles = makeStyles<void, 'controls'>()((theme, _params, classes) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.palette.largeVideoBackground,
|
||||
color: theme.palette.text01,
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
width: '100vw',
|
||||
|
||||
'& *, & *::before, & *::after': {
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
},
|
||||
videoArea: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.largeVideoBackground,
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
|
||||
[`&:hover .${classes.controls}`]: {
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto'
|
||||
}
|
||||
},
|
||||
videoElement: {
|
||||
backgroundColor: theme.palette.largeVideoBackground,
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
width: '100%'
|
||||
},
|
||||
avatarPlaceholder: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.largeVideoPlaceholder,
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
floatingControls: {
|
||||
alignItems: 'center',
|
||||
bottom: theme.spacing(4),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 10
|
||||
},
|
||||
participantName: {
|
||||
...getStageParticipantTypography(theme),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
width: '100%'
|
||||
},
|
||||
controls: {
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
transition: 'opacity 0.3s ease',
|
||||
|
||||
'& .toolbox-icon.hangup-button': {
|
||||
backgroundColor: theme.palette.actionDanger,
|
||||
|
||||
'@media (hover: hover) and (pointer: fine)': {
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.actionDangerHover
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
backgroundColor: theme.palette.actionDangerActive
|
||||
}
|
||||
},
|
||||
|
||||
'& svg': {
|
||||
fill: theme.palette.icon01
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the participant video, avatar fallback, display name and controls inside the Document PiP window.
|
||||
*
|
||||
* @returns {React.ReactElement}
|
||||
*/
|
||||
export function DocumentPiPView() {
|
||||
const { classes, cx } = useStyles();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const isOnPrejoin = useSelector(isPrejoinPageVisible);
|
||||
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
|
||||
const participant = isOnPrejoin ? localParticipant : largeVideoParticipant;
|
||||
const videoTrack = useSelector((state: IReduxState) => getPiPVideoTrack(state, participant));
|
||||
const participantName = useSelector((state: IReduxState) =>
|
||||
participant?.id ? getParticipantDisplayName(state, participant.id) : '');
|
||||
const shouldShowAvatar = shouldShowPiPAvatar(videoTrack);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
|
||||
if (!video || shouldShowAvatar || !videoTrack?.jitsiTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoTrack.jitsiTrack.attach(video).catch((error: Error) => {
|
||||
logger.error('Failed to attach video track in Document PiP:', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
try {
|
||||
videoTrack.jitsiTrack.detach(video);
|
||||
} catch (error) {
|
||||
logger.error('Failed to detach video track in Document PiP:', error);
|
||||
}
|
||||
};
|
||||
}, [ videoTrack, shouldShowAvatar ]);
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div className = { classes.videoArea }>
|
||||
{shouldShowAvatar ? (
|
||||
<div className = { classes.avatarPlaceholder }>
|
||||
<Avatar
|
||||
participantId = { participant?.id }
|
||||
size = { 120 } />
|
||||
</div>
|
||||
) : (
|
||||
<video
|
||||
autoPlay = { true }
|
||||
className = { classes.videoElement }
|
||||
muted = { true }
|
||||
playsInline = { true }
|
||||
ref = { videoRef } />
|
||||
)}
|
||||
<div className = { classes.floatingControls }>
|
||||
{participantName && <div className = { classes.participantName }>
|
||||
<DisplayNameBadge name = { participantName } />
|
||||
</div>}
|
||||
<div className = { cx(classes.controls, 'toolbox-content-items') }>
|
||||
<AudioMuteButton
|
||||
disableTooltip = { true }
|
||||
registerKeyboardShortcut = { false } />
|
||||
<VideoMuteButton
|
||||
disableTooltip = { true }
|
||||
registerKeyboardShortcut = { false } />
|
||||
<HangupButton
|
||||
customClass = 'hangup-button'
|
||||
disableTooltip = { true } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
react/features/pip/components/web/PiPTriggerButton.tsx
Normal file
65
react/features/pip/components/web/PiPTriggerButton.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconPip } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { togglePip } from '../../actions';
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is active.
|
||||
*/
|
||||
_isPiPActive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or closes Picture-in-Picture using the implementation supported by the browser.
|
||||
*/
|
||||
class PiPTriggerButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.pip';
|
||||
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.pipClose';
|
||||
override label = 'toolbar.pip';
|
||||
override toggledLabel = 'toolbar.pipClose';
|
||||
override tooltip = 'toolbar.pip';
|
||||
override toggledTooltip = 'toolbar.pipClose';
|
||||
override icon = IconPip;
|
||||
|
||||
/**
|
||||
* Indicates whether the button is toggled.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _isToggled(): boolean {
|
||||
return Boolean(this.props._isPiPActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles toggling Picture-in-Picture.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('toggle.pip', { enable: !this._isToggled() }));
|
||||
dispatch(togglePip());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps Redux state to component props.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_isPiPActive: Boolean(state['features/pip']?.isPiPActive)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(PiPTriggerButton));
|
||||
Reference in New Issue
Block a user