fix(shared-music): render player and add mobile/native support
- Add SharedMusicPlayer to LargeVideo.web.tsx so audio actually plays - Handle mobile autoplay restrictions in DirectAudioManager with fallback to play on user interaction - Add native (React Native) components for SharedMusicDialog and SharedMusicButton to support iOS/Android apps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1c9fed8794
commit
dfebe26cd1
|
|
@ -15,6 +15,7 @@ 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';
|
||||
|
|
@ -218,6 +219,7 @@ class LargeVideo extends Component<IProps> {
|
|||
ref = { this._containerRef }
|
||||
style = { style }>
|
||||
<SharedVideo />
|
||||
<SharedMusicPlayer />
|
||||
{_whiteboardEnabled && <Whiteboard />}
|
||||
<div id = 'etherpad' />
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export { default as SharedMusicDialog } from './native/SharedMusicDialog';
|
||||
export { default as SharedMusicButton } from './native/SharedMusicButton';
|
||||
|
||||
// SharedMusicPlayer is web-only - no native implementation yet
|
||||
export const SharedMusicPlayer = null;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconAudioOnly } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { toggleSharedMusic } from '../../actions';
|
||||
import { isSharingStatus } from '../../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SharedMusicButton}.
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether or not the button is disabled.
|
||||
*/
|
||||
_isDisabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is sharing music.
|
||||
*/
|
||||
_sharingMusic: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for sharing music.
|
||||
*
|
||||
* @augments AbstractButton
|
||||
*/
|
||||
class SharedMusicButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.sharedmusic';
|
||||
override icon = IconAudioOnly;
|
||||
override label = 'toolbar.sharedmusic';
|
||||
override toggledLabel = 'toolbar.stopSharedMusic';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
this._doToggleSharedMusic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _isToggled() {
|
||||
return this.props._sharingMusic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is disabled or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _isDisabled() {
|
||||
return this.props._isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle music sharing.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleSharedMusic() {
|
||||
this.props.dispatch(toggleSharedMusic());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component instance.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { ownerId, status: sharedMusicStatus } = state['features/shared-music'];
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const { visible = true } = ownProps;
|
||||
|
||||
if (ownerId !== localParticipantId) {
|
||||
return {
|
||||
_isDisabled: isSharingStatus(sharedMusicStatus ?? ''),
|
||||
_sharingMusic: false,
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
_isDisabled: false,
|
||||
_sharingMusic: isSharingStatus(sharedMusicStatus ?? ''),
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(SharedMusicButton));
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { hideDialog } from '../../../base/dialog/actions';
|
||||
import InputDialog from '../../../base/dialog/components/native/InputDialog';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { extractMusicUrl } from '../../functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Invoked to update the shared music link.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Function to be invoked after typing a valid URL.
|
||||
*/
|
||||
onPostSubmit: Function;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the music share dialog for native.
|
||||
*/
|
||||
class SharedMusicDialog extends Component<IProps, IState> {
|
||||
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: false
|
||||
};
|
||||
|
||||
this._onSubmitValue = this._onSubmitValue.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the value of the link input is submitted.
|
||||
*
|
||||
* @param {string} value - The entered music link.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSubmitValue(value: string) {
|
||||
const { onPostSubmit, dispatch } = this.props;
|
||||
const extracted = extractMusicUrl(value);
|
||||
|
||||
if (!extracted) {
|
||||
this.setState({ error: true });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onPostSubmit(value);
|
||||
dispatch(hideDialog());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { t } = this.props;
|
||||
const { error } = this.state;
|
||||
|
||||
return (
|
||||
<InputDialog
|
||||
messageKey = { error ? 'dialog.sharedMusicDialogError' : undefined }
|
||||
onSubmit = { this._onSubmitValue }
|
||||
textInputProps = {{
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: false,
|
||||
placeholder: t('dialog.sharedMusicLinkPlaceholder')
|
||||
}}
|
||||
titleKey = 'dialog.shareMusicTitle' />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} _state - The Redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(_state: IReduxState) {
|
||||
return {};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(SharedMusicDialog));
|
||||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { PLAYBACK_STATUSES } from '../../constants';
|
||||
import logger from '../../logger';
|
||||
|
||||
import AbstractMusicManager, {
|
||||
IProps,
|
||||
|
|
@ -15,6 +16,7 @@ import AbstractMusicManager, {
|
|||
*/
|
||||
class DirectAudioManager extends AbstractMusicManager {
|
||||
audioRef: React.RefObject<HTMLAudioElement>;
|
||||
_autoplayBlocked: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new DirectAudioManager instance.
|
||||
|
|
@ -26,6 +28,7 @@ class DirectAudioManager extends AbstractMusicManager {
|
|||
super(props);
|
||||
|
||||
this.audioRef = React.createRef();
|
||||
this._autoplayBlocked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -98,12 +101,40 @@ class DirectAudioManager extends AbstractMusicManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Plays music.
|
||||
* Plays music. Handles autoplay restrictions on mobile browsers.
|
||||
*
|
||||
* @returns {void}
|
||||
* @returns {Promise<void> | undefined}
|
||||
*/
|
||||
override play() {
|
||||
return this.player?.play();
|
||||
const playPromise = this.player?.play();
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch((error: Error) => {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
logger.warn('Autoplay blocked by browser. Music will play on user interaction.');
|
||||
this._autoplayBlocked = true;
|
||||
|
||||
// Add a one-time click listener to resume playback
|
||||
const resumePlayback = () => {
|
||||
if (this._autoplayBlocked && this.player) {
|
||||
this.player.play().catch(() => {
|
||||
// Ignore if still blocked
|
||||
});
|
||||
this._autoplayBlocked = false;
|
||||
}
|
||||
document.removeEventListener('click', resumePlayback);
|
||||
document.removeEventListener('touchstart', resumePlayback);
|
||||
};
|
||||
|
||||
document.addEventListener('click', resumePlayback, { once: true });
|
||||
document.addEventListener('touchstart', resumePlayback, { once: true });
|
||||
} else {
|
||||
logger.error('Error playing music:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return playPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue