mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(voice): point the corner volume at the focused stream (#3157)
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as VoiceSettingsCommands from '@app/features/voice/commands/VoiceSettingsCommands';
|
||||
import {
|
||||
callVolumePercentToSliderVolume,
|
||||
resolveCallVolumeMuteToggle,
|
||||
resolveLastNonZeroCallVolume,
|
||||
sliderVolumeToCallVolumePercent,
|
||||
} from '@app/features/voice/components/CallVolumeState';
|
||||
import {MediaVerticalVolumeControl} from '@app/features/voice/components/media_player/components/MediaVerticalVolumeControl';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {VOICE_OUTPUT_VOLUME_DESCRIPTOR} from '@app/features/voice/utils/VoiceMessageDescriptors';
|
||||
import {VOICE_VOLUME_MAX_SLIDER_VOLUME} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
|
||||
interface CallVolumeControlProps {
|
||||
className?: string;
|
||||
position?: 'above' | 'below';
|
||||
iconSize?: number;
|
||||
}
|
||||
|
||||
export const CallVolumeControl: React.FC<CallVolumeControlProps> = observer(function CallVolumeControl({
|
||||
className,
|
||||
position = 'above',
|
||||
iconSize = 18,
|
||||
}) {
|
||||
const {i18n} = useLingui();
|
||||
const outputVolume = VoiceSettings.outputVolume;
|
||||
const lastNonZeroVolumeRef = useRef(resolveLastNonZeroCallVolume(outputVolume, 0));
|
||||
useEffect(() => {
|
||||
lastNonZeroVolumeRef.current = resolveLastNonZeroCallVolume(outputVolume, lastNonZeroVolumeRef.current);
|
||||
}, [outputVolume]);
|
||||
const handleVolumeChange = useCallback((volume: number) => {
|
||||
const nextPercent = sliderVolumeToCallVolumePercent(volume);
|
||||
if (nextPercent === VoiceSettings.outputVolume) return;
|
||||
VoiceSettingsCommands.update({outputVolume: nextPercent});
|
||||
}, []);
|
||||
const handleToggleMute = useCallback(() => {
|
||||
VoiceSettingsCommands.update({
|
||||
outputVolume: resolveCallVolumeMuteToggle(VoiceSettings.outputVolume, lastNonZeroVolumeRef.current),
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<MediaVerticalVolumeControl
|
||||
volume={callVolumePercentToSliderVolume(outputVolume)}
|
||||
isMuted={outputVolume === 0}
|
||||
maxVolume={VOICE_VOLUME_MAX_SLIDER_VOLUME}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onToggleMute={handleToggleMute}
|
||||
iconSize={iconSize}
|
||||
className={className}
|
||||
position={position}
|
||||
ariaLabel={i18n._(VOICE_OUTPUT_VOLUME_DESCRIPTOR)}
|
||||
data-flx="voice.call-volume-control.media-vertical-volume-control"
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {VOICE_VOLUME_MAX_PERCENT} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||
|
||||
export const CALL_VOLUME_DEFAULT_PERCENT = 100;
|
||||
export const CALL_VOLUME_UNITY_PERCENT = 100;
|
||||
export const CALL_VOLUME_SLIDER_MAX_PERCENT = VOICE_VOLUME_MAX_PERCENT;
|
||||
|
||||
export function callVolumePercentToSliderVolume(percent: number): number {
|
||||
if (!Number.isFinite(percent)) {
|
||||
return CALL_VOLUME_DEFAULT_PERCENT / CALL_VOLUME_UNITY_PERCENT;
|
||||
}
|
||||
const clamped = Math.max(0, Math.min(CALL_VOLUME_SLIDER_MAX_PERCENT, percent));
|
||||
return clamped / CALL_VOLUME_UNITY_PERCENT;
|
||||
}
|
||||
|
||||
export function sliderVolumeToCallVolumePercent(volume: number): number {
|
||||
if (!Number.isFinite(volume)) {
|
||||
return CALL_VOLUME_DEFAULT_PERCENT;
|
||||
}
|
||||
const percent = Math.round(volume * CALL_VOLUME_UNITY_PERCENT);
|
||||
return Math.max(0, Math.min(CALL_VOLUME_SLIDER_MAX_PERCENT, percent));
|
||||
}
|
||||
|
||||
export function resolveLastNonZeroCallVolume(currentPercent: number, previousLastNonZero: number): number {
|
||||
if (currentPercent > 0) {
|
||||
return currentPercent;
|
||||
}
|
||||
if (previousLastNonZero > 0) {
|
||||
return previousLastNonZero;
|
||||
}
|
||||
return CALL_VOLUME_DEFAULT_PERCENT;
|
||||
}
|
||||
|
||||
export function resolveCallVolumeMuteToggle(currentPercent: number, lastNonZeroPercent: number): number {
|
||||
if (currentPercent > 0) {
|
||||
return 0;
|
||||
}
|
||||
if (lastNonZeroPercent > 0) {
|
||||
return lastNonZeroPercent;
|
||||
}
|
||||
return CALL_VOLUME_DEFAULT_PERCENT;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {STREAM_VOLUME_DESCRIPTOR} from '@app/features/ui/action_menu/items/voice_participant_menu_data/shared';
|
||||
import {MediaVerticalVolumeControl} from '@app/features/voice/components/media_player/components/MediaVerticalVolumeControl';
|
||||
import {getStreamKey} from '@app/features/voice/components/StreamKeys';
|
||||
import MediaEngine, {useMediaEngineVersion} from '@app/features/voice/engine/MediaEngineFacade';
|
||||
import {
|
||||
asVoiceTrackSource,
|
||||
isScreenShareAudioPublicationLike,
|
||||
VoiceTrackSource,
|
||||
} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import StreamAudioPrefs from '@app/features/voice/state/StreamAudioPrefs';
|
||||
import {parseVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity';
|
||||
import {VOICE_VOLUME_MAX_SLIDER_VOLUME} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import type {TrackReferenceOrPlaceholder} from '@livekit/components-react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback} from 'react';
|
||||
|
||||
interface FocusedStreamVolumeControlProps {
|
||||
track: TrackReferenceOrPlaceholder | null;
|
||||
guildId: string | null | undefined;
|
||||
channelId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FocusedStreamVolumeControl: React.FC<FocusedStreamVolumeControlProps> = observer(
|
||||
function FocusedStreamVolumeControl({track, guildId, channelId, className}) {
|
||||
const {i18n} = useLingui();
|
||||
useMediaEngineVersion();
|
||||
const isRemoteScreenShare =
|
||||
track != null && asVoiceTrackSource(track.source) === VoiceTrackSource.ScreenShare && !track.participant.isLocal;
|
||||
const {userId, connectionId} = isRemoteScreenShare
|
||||
? parseVoiceParticipantIdentity(track.participant.identity)
|
||||
: {userId: null, connectionId: null};
|
||||
const streamKey = connectionId ? getStreamKey(guildId, channelId, connectionId) : '';
|
||||
const hasStreamAudio =
|
||||
isRemoteScreenShare &&
|
||||
[...track.participant.audioTrackPublications.values()].some((publication) =>
|
||||
isScreenShareAudioPublicationLike(publication),
|
||||
);
|
||||
const isMuted = streamKey ? StreamAudioPrefs.isMuted(streamKey) : false;
|
||||
const handleToggleMute = useCallback(() => {
|
||||
if (!streamKey || !userId) return;
|
||||
StreamAudioPrefs.setMuted(streamKey, !StreamAudioPrefs.isMuted(streamKey));
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(userId);
|
||||
}, [streamKey, userId]);
|
||||
const handleVolumeChange = useCallback(
|
||||
(volume: number) => {
|
||||
if (!streamKey || !userId) return;
|
||||
StreamAudioPrefs.setVolume(streamKey, Math.round(volume * 100));
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(userId);
|
||||
},
|
||||
[streamKey, userId],
|
||||
);
|
||||
if (!streamKey || !userId || !hasStreamAudio) return null;
|
||||
return (
|
||||
<MediaVerticalVolumeControl
|
||||
volume={StreamAudioPrefs.getVolume(streamKey) / 100}
|
||||
isMuted={isMuted}
|
||||
maxVolume={VOICE_VOLUME_MAX_SLIDER_VOLUME}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onToggleMute={handleToggleMute}
|
||||
iconSize={18}
|
||||
className={className}
|
||||
position="above"
|
||||
ariaLabel={i18n._(STREAM_VOLUME_DESCRIPTOR)}
|
||||
data-flx="voice.focused-stream-volume-control.media-vertical-volume-control"
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ChannelHeaderIcon} from '@app/features/channel/components/channel_header_components/ChannelHeaderIcon';
|
||||
import {CallVolumeControl} from '@app/features/voice/components/CallVolumeControl';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import type {Icon} from '@phosphor-icons/react';
|
||||
@@ -54,12 +53,7 @@ export const VoiceCallCornerControls: React.FC<VoiceCallCornerControlsProps> = (
|
||||
}, []);
|
||||
return (
|
||||
<div className={wrapClassName} data-flx="voice.voice-call-corner-controls.wrap">
|
||||
{volumeControl ?? (
|
||||
<CallVolumeControl
|
||||
className={buttonClassName}
|
||||
data-flx="voice.voice-call-corner-controls.call-volume-control"
|
||||
/>
|
||||
)}
|
||||
{volumeControl}
|
||||
{showPopout && onPopOut && (
|
||||
<ChannelHeaderIcon
|
||||
icon={PopOutIcon}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import Favorites from '@app/features/messaging/state/Favorites';
|
||||
import {goBackOr} from '@app/features/platform/components/router/NavigationAdapter';
|
||||
import {STREAM_VOLUME_DESCRIPTOR} from '@app/features/ui/action_menu/items/voice_participant_menu_data/shared';
|
||||
import {BottomSheet} from '@app/features/ui/bottom_sheet/BottomSheet';
|
||||
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
@@ -29,7 +28,7 @@ import Popout from '@app/features/ui/state/Popout';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
||||
import {CompactVoiceCallView} from '@app/features/voice/components/CompactVoiceCallView';
|
||||
import {MediaVerticalVolumeControl} from '@app/features/voice/components/media_player/components/MediaVerticalVolumeControl';
|
||||
import {FocusedStreamVolumeControl} from '@app/features/voice/components/FocusedStreamVolumeControl';
|
||||
import {PoppedOutOverlay} from '@app/features/voice/components/popout/PoppedOutOverlay';
|
||||
import {
|
||||
selectPoppedOutOverlayTransition,
|
||||
@@ -57,26 +56,20 @@ import {
|
||||
import {VoiceRegionTeleportOverlay} from '@app/features/voice/components/VoiceRegionTeleportOverlay';
|
||||
import {VoiceDetailsPopout} from '@app/features/voice/components/voice_connection_status/VoiceDetailsPopout';
|
||||
import {VoiceDebugStatsForwarder} from '@app/features/voice/diagnostics/VoiceDebugStatsForwarder';
|
||||
import MediaEngine, {useMediaEngineVersion} from '@app/features/voice/engine/MediaEngineFacade';
|
||||
import {useMediaEngineVersion} from '@app/features/voice/engine/MediaEngineFacade';
|
||||
import {
|
||||
asVoiceEngineConnectionState,
|
||||
VoiceEngineConnectionState,
|
||||
} from '@app/features/voice/engine/VoiceConnectionStateMachine';
|
||||
import {
|
||||
asVoiceTrackSource,
|
||||
isScreenShareAudioPublicationLike,
|
||||
VoiceTrackSource,
|
||||
} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import {asVoiceTrackSource, VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import PopoutWindowManager, {
|
||||
getVoiceCallPopoutKey,
|
||||
isVoicePopoutSupported,
|
||||
} from '@app/features/voice/state/PopoutWindowManager';
|
||||
import StreamAudioPrefs from '@app/features/voice/state/StreamAudioPrefs';
|
||||
import VoiceCallLayout from '@app/features/voice/state/VoiceCallLayout';
|
||||
import {hasValidRoomForVoiceCallContext} from '@app/features/voice/utils/VoiceCallContext';
|
||||
import {VOICE_CALL_DESCRIPTOR} from '@app/features/voice/utils/VoiceMessageDescriptors';
|
||||
import {parseVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity';
|
||||
import {VOICE_VOLUME_MAX_SLIDER_VOLUME} from '@app/features/voice/utils/VoiceVolumeUtils';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
import {msg, plural} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
@@ -282,13 +275,6 @@ const VoiceCallViewInner = observer(
|
||||
return getStreamKey(channel.guildId, channel.id, focusedStreamInfo.connectionId);
|
||||
}, [focusedStreamInfo, channel.guildId, channel.id]);
|
||||
const focusedStreamTrackInfo = useStreamTrackInfo(isFocusedOnScreenShare ? effectiveFocusMainTrack : null);
|
||||
const hasFocusedStreamAudio = Boolean(
|
||||
effectiveFocusMainTrack &&
|
||||
[...effectiveFocusMainTrack.participant.audioTrackPublications.values()].some((publication) =>
|
||||
isScreenShareAudioPublicationLike(publication),
|
||||
),
|
||||
);
|
||||
const canControlFocusedStreamVolume = focusedStreamInfo !== null && !effectiveFocusMainTrack?.participant.isLocal;
|
||||
const focusedStreamerDisplayName = useMemo(() => {
|
||||
if (!focusedStreamerUser) return '';
|
||||
return NicknameUtils.getNickname(focusedStreamerUser, channel.guildId, channel.id);
|
||||
@@ -297,21 +283,6 @@ const VoiceCallViewInner = observer(
|
||||
focusedStreamKey,
|
||||
focusedStreamInfo?.userId,
|
||||
);
|
||||
const focusedStreamVolume = StreamAudioPrefs.getVolume(focusedStreamKey);
|
||||
const isFocusedStreamMuted = StreamAudioPrefs.isMuted(focusedStreamKey);
|
||||
const handleFocusedStreamToggleMute = useCallback(() => {
|
||||
if (!focusedStreamKey || !focusedStreamInfo) return;
|
||||
StreamAudioPrefs.setMuted(focusedStreamKey, !isFocusedStreamMuted);
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(focusedStreamInfo.userId);
|
||||
}, [focusedStreamKey, isFocusedStreamMuted, focusedStreamInfo]);
|
||||
const handleFocusedStreamVolumeChange = useCallback(
|
||||
(newVolume: number) => {
|
||||
if (!focusedStreamKey || !focusedStreamInfo) return;
|
||||
StreamAudioPrefs.setVolume(focusedStreamKey, Math.round(newVolume * 100));
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(focusedStreamInfo.userId);
|
||||
},
|
||||
[focusedStreamKey, focusedStreamInfo],
|
||||
);
|
||||
const handleSpectatorsPopoutOpenChange = useCallback((open: boolean) => {
|
||||
setIsSpectatorsPopoutOpen(open);
|
||||
}, []);
|
||||
@@ -695,20 +666,13 @@ const VoiceCallViewInner = observer(
|
||||
fullscreenIcon={FullscreenButtonIcon}
|
||||
onToggleFullscreen={handleToggleVoiceCallAppFullscreen}
|
||||
volumeControl={
|
||||
isFocusedOnScreenShare && focusedStreamKey && hasFocusedStreamAudio && canControlFocusedStreamVolume ? (
|
||||
<MediaVerticalVolumeControl
|
||||
volume={focusedStreamVolume / 100}
|
||||
isMuted={isFocusedStreamMuted}
|
||||
maxVolume={VOICE_VOLUME_MAX_SLIDER_VOLUME}
|
||||
onVolumeChange={handleFocusedStreamVolumeChange}
|
||||
onToggleMute={handleFocusedStreamToggleMute}
|
||||
iconSize={18}
|
||||
className={styles.voiceHeaderIconButton}
|
||||
position="above"
|
||||
ariaLabel={i18n._(STREAM_VOLUME_DESCRIPTOR)}
|
||||
data-flx="voice.voice-call-view.voice-call-view-inner.stream-volume-control"
|
||||
/>
|
||||
) : undefined
|
||||
<FocusedStreamVolumeControl
|
||||
track={isFocusedOnScreenShare ? effectiveFocusMainTrack : null}
|
||||
guildId={channel.guildId}
|
||||
channelId={channel.id}
|
||||
className={styles.voiceHeaderIconButton}
|
||||
data-flx="voice.voice-call-view.voice-call-view-inner.focused-stream-volume-control"
|
||||
/>
|
||||
}
|
||||
data-flx="voice.voice-call-view.voice-call-view-inner.voice-call-corner-controls"
|
||||
/>
|
||||
|
||||
+9
@@ -27,6 +27,7 @@ import {
|
||||
} from '@app/features/voice/components/compact_voice_call_view/shared';
|
||||
import {useCompactVoiceCallResize} from '@app/features/voice/components/compact_voice_call_view/useCompactVoiceCallResize';
|
||||
import {useConnectionLabel} from '@app/features/voice/components/compact_voice_call_view/useConnectionLabel';
|
||||
import {FocusedStreamVolumeControl} from '@app/features/voice/components/FocusedStreamVolumeControl';
|
||||
import {PoppedOutOverlay} from '@app/features/voice/components/popout/PoppedOutOverlay';
|
||||
import {
|
||||
selectPoppedOutOverlayTransition,
|
||||
@@ -523,6 +524,14 @@ export const CompactVoiceCallViewInner: React.FC<CompactVoiceCallViewProps> = ob
|
||||
fullscreenLabel={fullscreenButtonLabel}
|
||||
fullscreenIcon={FullscreenButtonIcon}
|
||||
onToggleFullscreen={handleToggleVoiceCallAppFullscreen}
|
||||
volumeControl={
|
||||
<FocusedStreamVolumeControl
|
||||
track={layoutMode === 'focus' ? focusMainTrack : null}
|
||||
guildId={channel.guildId}
|
||||
channelId={channel.id}
|
||||
data-flx="voice.compact-voice-call-view.compact-voice-call-view-inner.focused-stream-volume-control"
|
||||
/>
|
||||
}
|
||||
data-flx="voice.compact-voice-call-view.compact-voice-call-view-inner.voice-call-corner-controls"
|
||||
/>
|
||||
{shouldRenderPoppedOutOverlay(poppedOutTransition.snapshot) && (
|
||||
|
||||
Reference in New Issue
Block a user