diff --git a/css/_shared_video.scss b/css/_shared_video.scss new file mode 100644 index 0000000..5fe1d48 --- /dev/null +++ b/css/_shared_video.scss @@ -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; +} diff --git a/css/main.scss b/css/main.scss index 8e7267b..9e55535 100644 --- a/css/main.scss +++ b/css/main.scss @@ -77,6 +77,7 @@ $flagsImagePath: "../images/"; @import 'reactions-menu'; @import 'plan-limit'; @import 'shared_music'; +@import 'shared_video'; @import 'meeting_intelligence'; /* Modules END */ diff --git a/react/features/filmstrip/components/web/Thumbnail.tsx b/react/features/filmstrip/components/web/Thumbnail.tsx index ae9eb8d..564883d 100644 --- a/react/features/filmstrip/components/web/Thumbnail.tsx +++ b/react/features/filmstrip/components/web/Thumbnail.tsx @@ -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 { ); } + /** + * 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 ( + + + + ); + } + /** * Renders the avatar. * @@ -1215,6 +1250,11 @@ class Thumbnail extends Component { return this._renderSharedMusicParticipant(); } + // Render SharedVideo with embedded player in tile view + if (isSharedVideoParticipant(_participant)) { + return this._renderSharedVideoParticipant(); + } + if (fakeParticipant && !isWhiteboardParticipant(_participant) && !_isVirtualScreenshareParticipant diff --git a/react/features/shared-video/components/index.web.ts b/react/features/shared-video/components/index.web.ts index af18c8e..acdf94c 100644 --- a/react/features/shared-video/components/index.web.ts +++ b/react/features/shared-video/components/index.web.ts @@ -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'; diff --git a/react/features/shared-video/components/web/SharedVideo.tsx b/react/features/shared-video/components/web/SharedVideo.tsx index 24ef661..341dd1f 100644 --- a/react/features/shared-video/components/web/SharedVideo.tsx +++ b/react/features/shared-video/components/web/SharedVideo.tsx @@ -131,11 +131,13 @@ class SharedVideo extends Component { } if (EMBED_VIDEO_TYPES.includes(sourceType as any)) { - return (); + return ( + + ); } return ; @@ -150,16 +152,12 @@ class SharedVideo extends Component { 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 (
= ({ 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 ( +