feat(shared-music): display YouTube video in tile view with controls
- Add SharedMusicTile component for rendering video in filmstrip - Create fake participant for shared music (shows as tile, not pinned) - Make YouTube player visible with proper dimensions in tile - Remove auto-mute behavior when sharing music - Add SharedMusic to FakeParticipant enum - Add isSharedMusicParticipant helper function - Move player rendering from LargeVideo to tile - Add CSS styles for player wrapper and controls overlay Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6fc0ae0b5
commit
751a04b950
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Styles for the shared music tile in the filmstrip.
|
||||
*/
|
||||
|
||||
.shared-music-tile {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shared-music-player-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
#sharedMusic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.youtube-player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shared-music-audio-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '🎵';
|
||||
font-size: 48px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.shared-music-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-music-title,
|
||||
.shared-music-control-button,
|
||||
.shared-music-status {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.shared-music-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;
|
||||
}
|
||||
|
||||
.shared-music-control-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
margin: 8px auto 0;
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #1a1a2e;
|
||||
}
|
||||
}
|
||||
|
||||
.shared-music-status {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Ensure YouTube iframe is interactive */
|
||||
.shared-music-player-wrapper iframe {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Hide the old styles that are no longer needed */
|
||||
.shared-music-thumbnail,
|
||||
.shared-music-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -76,5 +76,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'participants-pane';
|
||||
@import 'reactions-menu';
|
||||
@import 'plan-limit';
|
||||
@import 'shared_music';
|
||||
|
||||
/* Modules END */
|
||||
|
|
|
|||
|
|
@ -349,6 +349,16 @@ export function isWhiteboardParticipant(participant?: IParticipant): boolean {
|
|||
return participant?.fakeParticipant === FakeParticipant.Whiteboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the passed in participant is a shared music participant.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant entity.
|
||||
* @returns {boolean} - True if it's a shared music participant.
|
||||
*/
|
||||
export function isSharedMusicParticipant(participant?: IParticipant): boolean {
|
||||
return participant?.fakeParticipant === FakeParticipant.SharedMusic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the known remote participants in the passed in redux state.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { IJitsiConference } from '../conference/reducer';
|
|||
export enum FakeParticipant {
|
||||
LocalScreenShare = 'LocalScreenShare',
|
||||
RemoteScreenShare = 'RemoteScreenShare',
|
||||
SharedMusic = 'SharedMusic',
|
||||
SharedVideo = 'SharedVideo',
|
||||
Whiteboard = 'Whiteboard'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
hasRaisedHand,
|
||||
isLocalScreenshareParticipant,
|
||||
isScreenShareParticipant,
|
||||
isSharedMusicParticipant,
|
||||
isWhiteboardParticipant
|
||||
} from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
|
|
@ -39,6 +40,7 @@ import { getVideoObjectPosition } from '../../../face-landmarks/functions';
|
|||
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 { LAYOUTS } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.web';
|
||||
import { togglePinStageParticipant } from '../../actions';
|
||||
|
|
@ -907,6 +909,39 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a shared music participant thumbnail with video and controls.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSharedMusicParticipant() {
|
||||
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 = 'sharedMusicContainer'
|
||||
onClick = { this._onClick }
|
||||
onKeyDown = { this._onTogglePinButtonKeyDown }
|
||||
{ ...(_isMobile ? {} : {
|
||||
onMouseEnter: this._onMouseEnter,
|
||||
onMouseMove: this._onMouseMove,
|
||||
onMouseLeave: this._onMouseLeave
|
||||
}) }
|
||||
role = 'button'
|
||||
style = { styles.thumbnail }
|
||||
tabIndex = { 0 }>
|
||||
<SharedMusicTile participantId = { id } />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the avatar.
|
||||
*
|
||||
|
|
@ -1183,6 +1218,11 @@ class Thumbnail extends Component<IProps, IState> {
|
|||
return this._renderParticipant(true);
|
||||
}
|
||||
|
||||
// Render SharedMusic with custom tile that shows video/controls
|
||||
if (isSharedMusicParticipant(_participant)) {
|
||||
return this._renderSharedMusicParticipant();
|
||||
}
|
||||
|
||||
if (fakeParticipant
|
||||
&& !isWhiteboardParticipant(_participant)
|
||||
&& !_isVirtualScreenshareParticipant
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { isSpotTV } from '../../base/util/spot';
|
|||
import StageParticipantNameLabel from '../../display-name/components/web/StageParticipantNameLabel';
|
||||
import { FILMSTRIP_BREAKPOINT } from '../../filmstrip/constants';
|
||||
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../filmstrip/functions.web';
|
||||
import { SharedMusicPlayer } from '../../shared-music/components';
|
||||
import SharedVideo from '../../shared-video/components/web/SharedVideo';
|
||||
import Captions from '../../subtitles/components/web/Captions';
|
||||
import { areClosedCaptionsEnabled } from '../../subtitles/functions.any';
|
||||
|
|
@ -219,7 +218,6 @@ class LargeVideo extends Component<IProps> {
|
|||
ref = { this._containerRef }
|
||||
style = { style }>
|
||||
<SharedVideo />
|
||||
<SharedMusicPlayer />
|
||||
{_whiteboardEnabled && <Whiteboard />}
|
||||
<div id = 'etherpad' />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as SharedMusicDialog } from './web/SharedMusicDialog';
|
||||
export { default as SharedMusicButton } from './web/SharedMusicButton';
|
||||
export { default as SharedMusicPlayer } from './web/SharedMusicPlayer';
|
||||
export { default as SharedMusicTile } from './web/SharedMusicTile';
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import { PureComponent } from 'react';
|
|||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { IJitsiConference } from '../../../base/conference/reducer';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { isLocalTrackMuted } from '../../../base/tracks/functions';
|
||||
import { showWarningNotification } from '../../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
|
||||
import { muteLocal } from '../../../video-menu/actions.any';
|
||||
import { setSharedMusicStatus, stopSharedMusic } from '../../actions';
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
import logger from '../../logger';
|
||||
|
|
@ -42,11 +39,6 @@ export interface IProps {
|
|||
*/
|
||||
_displayWarning: Function;
|
||||
|
||||
/**
|
||||
* Indicates whether the local audio is muted.
|
||||
*/
|
||||
_isLocalAudioMuted: boolean;
|
||||
|
||||
/**
|
||||
* Is the music shared by the local user.
|
||||
*/
|
||||
|
|
@ -57,11 +49,6 @@ export interface IProps {
|
|||
*/
|
||||
_musicUrl?: string;
|
||||
|
||||
/**
|
||||
* Mutes local audio track.
|
||||
*/
|
||||
_muteLocal: Function;
|
||||
|
||||
/**
|
||||
* Store flag for muted state.
|
||||
*/
|
||||
|
|
@ -205,7 +192,6 @@ class AbstractMusicManager extends PureComponent<IProps> {
|
|||
* @returns {void}
|
||||
*/
|
||||
onPlay() {
|
||||
this.smartAudioMute();
|
||||
this.fireUpdateSharedMusicEvent();
|
||||
}
|
||||
|
||||
|
|
@ -224,13 +210,6 @@ class AbstractMusicManager extends PureComponent<IProps> {
|
|||
* @returns {void}
|
||||
*/
|
||||
onVolumeChange() {
|
||||
const volume = this.getVolume();
|
||||
const muted = this.isMuted();
|
||||
|
||||
if (Number(volume) > 0 && !muted) {
|
||||
this.smartAudioMute();
|
||||
}
|
||||
|
||||
this.fireUpdatePlayingMusicEvent();
|
||||
}
|
||||
|
||||
|
|
@ -280,34 +259,6 @@ class AbstractMusicManager extends PureComponent<IProps> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the player volume is currently on. This will return true if
|
||||
* we have an available player, which is currently in a PLAYING state,
|
||||
* which isn't muted and has its volume greater than 0.
|
||||
*
|
||||
* @returns {boolean} Indicating if the volume of the shared music is
|
||||
* currently on.
|
||||
*/
|
||||
isSharedMusicVolumeOn() {
|
||||
return this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING
|
||||
&& !this.isMuted()
|
||||
&& Number(this.getVolume()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart mike mute. If the mike isn't currently muted and the shared music
|
||||
* volume is on we mute the mike.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
smartAudioMute() {
|
||||
const { _isLocalAudioMuted, _muteLocal } = this.props;
|
||||
|
||||
if (!_isLocalAudioMuted && this.isSharedMusicVolumeOn()) {
|
||||
_muteLocal(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks music to provided time.
|
||||
*
|
||||
|
|
@ -412,11 +363,9 @@ export default AbstractMusicManager;
|
|||
export function _mapStateToProps(state: IReduxState) {
|
||||
const { ownerId, status, time, musicUrl, muted, sourceType } = state['features/shared-music'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const _isLocalAudioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
|
||||
|
||||
return {
|
||||
_conference: getCurrentConference(state),
|
||||
_isLocalAudioMuted,
|
||||
_isOwner: ownerId === localParticipant?.id,
|
||||
_muted: muted,
|
||||
_musicUrl: musicUrl,
|
||||
|
|
@ -443,9 +392,6 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
|||
_stopSharedMusic: () => {
|
||||
dispatch(stopSharedMusic());
|
||||
},
|
||||
_muteLocal: (value: boolean) => {
|
||||
dispatch(muteLocal(value, MEDIA_TYPE.AUDIO));
|
||||
},
|
||||
_setSharedMusicStatus: ({ musicUrl, status, time, ownerId, muted, sourceType }: any) => {
|
||||
dispatch(setSharedMusicStatus({
|
||||
musicUrl,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconPlay, IconStop } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { setSharedMusicStatus } from '../../actions';
|
||||
import { PLAYBACK_STATUSES, SOURCE_TYPES } from '../../constants';
|
||||
import { isSharingStatus } from '../../functions';
|
||||
|
||||
import SharedMusicPlayer from './SharedMusicPlayer';
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* The participant ID (music URL).
|
||||
*/
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a shared music tile with the actual video player.
|
||||
* This is displayed in the filmstrip thumbnail for shared music.
|
||||
*
|
||||
* @param {IProps} props - The component props.
|
||||
* @returns {React.ReactElement | null}
|
||||
*/
|
||||
const SharedMusicTile: React.FC<IProps> = ({ participantId }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { musicUrl, ownerId, status, sourceType, time, title } = useSelector(
|
||||
(state: IReduxState) => state['features/shared-music']
|
||||
);
|
||||
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
|
||||
const isOwner = ownerId === localParticipant?.id;
|
||||
const isMusicShared = isSharingStatus(status ?? '');
|
||||
const isPlaying = status === PLAYBACK_STATUSES.PLAYING;
|
||||
const isYouTube = sourceType === SOURCE_TYPES.YOUTUBE;
|
||||
|
||||
const handlePlayPause = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent thumbnail click from pinning
|
||||
|
||||
if (!isOwner || !musicUrl) {
|
||||
return; // Only owner can control playback, and musicUrl must exist
|
||||
}
|
||||
|
||||
const newStatus = isPlaying ? PLAYBACK_STATUSES.PAUSED : PLAYBACK_STATUSES.PLAYING;
|
||||
|
||||
dispatch(setSharedMusicStatus({
|
||||
musicUrl,
|
||||
status: newStatus,
|
||||
time: time ?? 0,
|
||||
ownerId,
|
||||
sourceType
|
||||
}));
|
||||
}, [ dispatch, isOwner, isPlaying, musicUrl, ownerId, sourceType, time ]);
|
||||
|
||||
if (!isMusicShared || participantId !== musicUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'shared-music-tile'>
|
||||
{/* Render the actual player for YouTube videos */}
|
||||
{isYouTube ? (
|
||||
<div className = 'shared-music-player-wrapper'>
|
||||
<SharedMusicPlayer />
|
||||
</div>
|
||||
) : (
|
||||
/* For audio-only, show a background with controls */
|
||||
<div className = 'shared-music-audio-bg'>
|
||||
<SharedMusicPlayer />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay with title and controls for non-owners */}
|
||||
<div className = 'shared-music-controls-overlay'>
|
||||
{/* Title */}
|
||||
<div className = 'shared-music-title'>
|
||||
{title || 'Shared Music'}
|
||||
</div>
|
||||
|
||||
{/* Play/Pause button (owner only, shown when YouTube controls are hidden) */}
|
||||
{isOwner && !isYouTube && (
|
||||
<button
|
||||
aria-label = { isPlaying ? 'Pause' : 'Play' }
|
||||
className = 'shared-music-control-button'
|
||||
onClick = { handlePlayPause }
|
||||
type = 'button'>
|
||||
{isPlaying ? (
|
||||
<IconStop />
|
||||
) : (
|
||||
<IconPlay />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Status indicator for non-owners when not YouTube */}
|
||||
{!isOwner && !isYouTube && (
|
||||
<div className = 'shared-music-status'>
|
||||
{isPlaying ? 'Playing' : 'Paused'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedMusicTile;
|
||||
|
|
@ -194,14 +194,15 @@ class YouTubeMusicManager extends AbstractMusicManager {
|
|||
const options = {
|
||||
id: 'sharedMusicPlayer',
|
||||
opts: {
|
||||
height: '1',
|
||||
width: '1',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
playerVars: {
|
||||
'origin': location.origin,
|
||||
'fs': '0',
|
||||
'autoplay': 0,
|
||||
'controls': showControls,
|
||||
'rel': 0
|
||||
'rel': 0,
|
||||
'modestbranding': 1
|
||||
}
|
||||
},
|
||||
onError: (e: any) => this.onError(e),
|
||||
|
|
@ -219,9 +220,9 @@ class YouTubeMusicManager extends AbstractMusicManager {
|
|||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
// Render the YouTube player hidden (offscreen) since we only need audio
|
||||
// Render the YouTube player visible for video playback in the tile
|
||||
return (
|
||||
<div className = 'okhide'>
|
||||
<div className = 'youtube-player-container'>
|
||||
<YouTube
|
||||
{ ...this.getPlayerOptions() } />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { CONFERENCE_JOIN_IN_PROGRESS, CONFERENCE_LEFT } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
import { participantJoined, participantLeft } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { RESET_SHARED_MUSIC_STATUS, SET_SHARED_MUSIC_STATUS } from './actionTypes';
|
||||
|
|
@ -13,6 +17,7 @@ import {
|
|||
setSharedMusicStatus
|
||||
} from './actions';
|
||||
import {
|
||||
MUSIC_PLAYER_PARTICIPANT_NAME,
|
||||
PLAYBACK_START,
|
||||
PLAYBACK_STATUSES,
|
||||
SHARED_MUSIC,
|
||||
|
|
@ -63,6 +68,12 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
}
|
||||
|
||||
if (sharedMusicStatus === 'stop') {
|
||||
const musicParticipant = getParticipantById(state, value);
|
||||
|
||||
dispatch(participantLeft(value, conference, {
|
||||
fakeParticipant: musicParticipant?.fakeParticipant
|
||||
}));
|
||||
|
||||
if (localParticipantId !== attributes.from) {
|
||||
dispatch(resetSharedMusicStatus());
|
||||
}
|
||||
|
|
@ -76,10 +87,14 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
case PARTICIPANT_LEFT: {
|
||||
const state = getState();
|
||||
const { ownerId: stateOwnerId } = state['features/shared-music'];
|
||||
const conference = getCurrentConference(state);
|
||||
const { ownerId: stateOwnerId, musicUrl: stateMusicUrl } = state['features/shared-music'];
|
||||
|
||||
if (action.participant.id === stateOwnerId) {
|
||||
dispatch(resetSharedMusicStatus());
|
||||
batch(() => {
|
||||
dispatch(resetSharedMusicStatus());
|
||||
dispatch(participantLeft(stateMusicUrl ?? '', conference));
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -154,17 +169,18 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
/**
|
||||
* Handles the playing, pause and start statuses for the shared music.
|
||||
* Dispatches participantJoined event to show music as a participant tile.
|
||||
* Sets the SharedMusicStatus if the event was triggered by the local user.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {string} musicUrl - The id/url of the music to be shared.
|
||||
* @param {Object} attributes - The attributes received from the share music command.
|
||||
* @param {JitsiConference} _conference - The current conference.
|
||||
* @param {JitsiConference} conference - The current conference.
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleSharingMusicStatus(store: IStore, musicUrl: string,
|
||||
attributes: IMusicCommandAttributes,
|
||||
_conference: IJitsiConference) {
|
||||
conference: IJitsiConference) {
|
||||
const { dispatch, getState } = store;
|
||||
const localParticipantId = getLocalParticipant(getState())?.id;
|
||||
const oldStatus = getState()['features/shared-music']?.status ?? '';
|
||||
|
|
@ -179,8 +195,22 @@ function handleSharingMusicStatus(store: IStore, musicUrl: string,
|
|||
|
||||
const sourceType = (attributes.sourceType as SourceType) || SOURCE_TYPES.DIRECT;
|
||||
|
||||
// If music was not started, set the initial status
|
||||
// If music was not started (no participant), create the fake participant
|
||||
// This can be triggered by start, playing, or paused commands (joining late)
|
||||
if (attributes.state === PLAYBACK_START || !isSharingStatus(oldStatus)) {
|
||||
// Create a fake participant for the music player (shows as a tile, not pinned)
|
||||
const displayName = attributes.title || MUSIC_PLAYER_PARTICIPANT_NAME;
|
||||
|
||||
dispatch(participantJoined({
|
||||
conference,
|
||||
fakeParticipant: FakeParticipant.SharedMusic,
|
||||
id: musicUrl,
|
||||
name: displayName
|
||||
}));
|
||||
|
||||
// Note: We intentionally do NOT pin the participant so it appears as a tile
|
||||
// instead of taking over the whole screen like shared video does
|
||||
|
||||
if (localParticipantId === attributes.from) {
|
||||
dispatch(setSharedMusicStatus({
|
||||
musicUrl,
|
||||
|
|
|
|||
Loading…
Reference in New Issue