feat(shared-video): show shared video in tile view and default to tile layout
Remove auto-exit from tile view when video is shared, stop auto-pinning the video participant, and render shared videos as embedded tiles in the filmstrip. Users can still click to pin/maximize the video to stage view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2fe77055a9
commit
2984a2944c
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Styles for the shared video tile in the filmstrip.
|
||||
*/
|
||||
|
||||
.shared-video-tile {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shared-video-player-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
iframe,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.shared-video-controls-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shared-video-title {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Ensure iframe is interactive for video controls */
|
||||
.shared-video-player-wrapper iframe,
|
||||
.shared-video-player-wrapper video {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ $flagsImagePath: "../images/";
|
|||
@import 'reactions-menu';
|
||||
@import 'plan-limit';
|
||||
@import 'shared_music';
|
||||
@import 'shared_video';
|
||||
@import 'meeting_intelligence';
|
||||
|
||||
/* Modules END */
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
isLocalScreenshareParticipant,
|
||||
isScreenShareParticipant,
|
||||
isSharedMusicParticipant,
|
||||
isSharedVideoParticipant,
|
||||
isWhiteboardParticipant
|
||||
} from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
|
|
@ -41,6 +42,7 @@ import { hideGif, showGif } from '../../../gifs/actions';
|
|||
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
|
||||
import PresenceLabel from '../../../presence-status/components/PresenceLabel';
|
||||
import { SharedMusicTile } from '../../../shared-music/components';
|
||||
import { SharedVideoTile } from '../../../shared-video/components';
|
||||
import { LAYOUTS } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.web';
|
||||
import { togglePinStageParticipant } from '../../actions';
|
||||
|
|
@ -934,6 +936,39 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a shared video participant thumbnail with embedded video player.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSharedVideoParticipant() {
|
||||
const { _isMobile, _participant } = this.props;
|
||||
const { id, pinned, name } = _participant;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-label = { this.props.t(pinned ? 'unpinParticipant' : 'pinParticipant', {
|
||||
participantName: name
|
||||
}) }
|
||||
className = { containerClassName }
|
||||
id = 'sharedVideoTileContainer'
|
||||
onClick = { this._onClick }
|
||||
onKeyDown = { this._onTogglePinButtonKeyDown }
|
||||
{ ...(_isMobile ? {} : {
|
||||
onMouseEnter: this._onMouseEnter,
|
||||
onMouseMove: this._onMouseMove,
|
||||
onMouseLeave: this._onMouseLeave
|
||||
}) }
|
||||
role = 'button'
|
||||
style = { styles.thumbnail }
|
||||
tabIndex = { 0 }>
|
||||
<SharedVideoTile participantId = { id } />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the avatar.
|
||||
*
|
||||
|
|
@ -1215,6 +1250,11 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
return this._renderSharedMusicParticipant();
|
||||
}
|
||||
|
||||
// Render SharedVideo with embedded player in tile view
|
||||
if (isSharedVideoParticipant(_participant)) {
|
||||
return this._renderSharedVideoParticipant();
|
||||
}
|
||||
|
||||
if (fakeParticipant
|
||||
&& !isWhiteboardParticipant(_participant)
|
||||
&& !_isVirtualScreenshareParticipant
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
|
||||
export { default as SharedVideoButton } from './web/SharedVideoButton';
|
||||
export { default as ShareVideoConfirmDialog } from './web/ShareVideoConfirmDialog';
|
||||
export { default as SharedVideoTile } from './web/SharedVideoTile';
|
||||
|
|
|
|||
|
|
@ -131,11 +131,13 @@ class SharedVideo extends Component<IProps> {
|
|||
}
|
||||
|
||||
if (EMBED_VIDEO_TYPES.includes(sourceType as any)) {
|
||||
return (<EmbedVideoManager
|
||||
return (
|
||||
<EmbedVideoManager
|
||||
// @ts-ignore
|
||||
sourceType = { sourceType }
|
||||
// @ts-ignore
|
||||
videoId = { videoUrl } />);
|
||||
videoId = { videoUrl } />
|
||||
);
|
||||
}
|
||||
|
||||
return <VideoManager videoId = { videoUrl } />;
|
||||
|
|
@ -150,16 +152,12 @@ class SharedVideo extends Component<IProps> {
|
|||
override render() {
|
||||
const { isEnabled, isResizing, isVideoShared, onStage } = this.props;
|
||||
|
||||
if (!isEnabled || !isVideoShared) {
|
||||
if (!isEnabled || !isVideoShared || !onStage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style: any = this.getDimensions();
|
||||
|
||||
if (!onStage) {
|
||||
style.display = 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { (isResizing && 'disable-pointer') || '' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { VIDEO_SOURCE_TYPES } from '../../constants';
|
||||
import { getVideoEmbedUrl, getVideoSourceType, isVideoPlaying } from '../../functions';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The participant ID (video URL or YouTube ID).
|
||||
*/
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a shared video tile in the filmstrip.
|
||||
* Displays the embedded video player inside the filmstrip thumbnail.
|
||||
*
|
||||
* @param {IProps} props - The component props.
|
||||
* @returns {React.ReactElement | null}
|
||||
*/
|
||||
const SharedVideoTile: React.FC<IProps> = ({ participantId }) => {
|
||||
const { videoUrl } = useSelector(
|
||||
(state: IReduxState) => state['features/shared-video']
|
||||
);
|
||||
const isShared = useSelector((state: IReduxState) => isVideoPlaying(state));
|
||||
|
||||
if (!isShared || participantId !== videoUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceType = getVideoSourceType(videoUrl ?? '');
|
||||
|
||||
const renderPlayer = () => {
|
||||
if (!videoUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sourceType === VIDEO_SOURCE_TYPES.YOUTUBE) {
|
||||
// videoUrl is a YouTube ID (not a full URL) for YouTube videos
|
||||
const youtubeId = videoUrl.match(/^https?:\/\//) ? null : videoUrl;
|
||||
const embedId = youtubeId ?? videoUrl;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
allow = 'autoplay; encrypted-media'
|
||||
allowFullScreen = { true }
|
||||
frameBorder = '0'
|
||||
height = '100%'
|
||||
src = { `https://www.youtube.com/embed/${embedId}?autoplay=1&controls=1&rel=0` }
|
||||
title = 'Shared Video'
|
||||
width = '100%' />
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceType === VIDEO_SOURCE_TYPES.VIMEO
|
||||
|| sourceType === VIDEO_SOURCE_TYPES.DAILYMOTION
|
||||
|| sourceType === VIDEO_SOURCE_TYPES.TWITCH) {
|
||||
const embedUrl = getVideoEmbedUrl(videoUrl, sourceType);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
allow = 'autoplay; encrypted-media; fullscreen'
|
||||
allowFullScreen = { true }
|
||||
frameBorder = '0'
|
||||
height = '100%'
|
||||
src = { embedUrl }
|
||||
title = 'Shared Video'
|
||||
width = '100%' />
|
||||
);
|
||||
}
|
||||
|
||||
// Direct video URL
|
||||
return (
|
||||
<video
|
||||
autoPlay = { true }
|
||||
controls = { true }
|
||||
height = '100%'
|
||||
src = { videoUrl }
|
||||
width = '100%'>
|
||||
<track kind = 'captions' />
|
||||
</video>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = 'shared-video-tile'>
|
||||
<div className = 'shared-video-player-wrapper'>
|
||||
{renderPlayer()}
|
||||
</div>
|
||||
<div className = 'shared-video-controls-overlay'>
|
||||
<div className = 'shared-video-title'>
|
||||
{'Shared Video'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedVideoTile;
|
||||
|
|
@ -7,7 +7,7 @@ import { IJitsiConference } from '../base/conference/reducer';
|
|||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
|
||||
import { participantJoined, participantLeft } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
|
@ -266,8 +266,6 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string,
|
|||
name: VIDEO_PLAYER_PARTICIPANT_NAME
|
||||
}));
|
||||
|
||||
dispatch(pinParticipant(videoUrl));
|
||||
|
||||
if (localParticipantId === from) {
|
||||
dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { pinParticipant } from '../base/participants/actions';
|
|||
import { getParticipantCount, getPinnedParticipant } from '../base/participants/functions';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import { isStageFilmstripAvailable, isTileViewModeDisabled } from '../filmstrip/functions';
|
||||
import { isVideoPlaying } from '../shared-video/functions';
|
||||
// isVideoPlaying no longer used to force exit from tile view
|
||||
import { VIDEO_QUALITY_LEVELS } from '../video-quality/constants';
|
||||
import { getReceiverVideoQualityLevel } from '../video-quality/functions';
|
||||
import { getMinHeightForQualityLvlMap } from '../video-quality/selector';
|
||||
|
|
@ -100,9 +100,6 @@ export function shouldDisplayTileView(state: IReduxState) {
|
|||
// It's a 1-on-1 meeting
|
||||
|| participantCount < 3
|
||||
|
||||
// There is a shared YouTube video in the meeting
|
||||
|| isVideoPlaying(state)
|
||||
|
||||
// We want jibri to use stage view by default
|
||||
|| iAmRecorder
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue