mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(voice): add audio bitrate guild features and 96 kbps cap (#2703)
This commit is contained in:
+13
-6
@@ -16,8 +16,10 @@ import {MatureContentSection} from '@app/features/channel/components/modals/chan
|
||||
import {RtcRegionSelect} from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/RtcRegionSelect';
|
||||
import {SlowmodeControl} from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl';
|
||||
import {
|
||||
BITRATE_KBPS_DEFAULT,
|
||||
CHANNEL_OVERVIEW_TAB_ID,
|
||||
type FormInputs,
|
||||
getMaxBitrateKbps,
|
||||
} from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/shared';
|
||||
import {
|
||||
VoiceConnectionLimitControl,
|
||||
@@ -81,6 +83,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
guildId !== null ? Permission.can(Permissions.UPDATE_RTC_REGION, {guildId, channelId}) : false;
|
||||
const canManageChannel = guildId !== null ? Permission.can(Permissions.MANAGE_CHANNELS, {guildId, channelId}) : false;
|
||||
const isVoiceChannel = channel?.type === ChannelTypes.GUILD_VOICE;
|
||||
const maxBitrateKbps = getMaxBitrateKbps(guild?.features);
|
||||
const [rtcRegions, setRtcRegions] = useState<Array<ChannelRtcRegion>>([]);
|
||||
const [isLoadingRegions, setIsLoadingRegions] = useState(false);
|
||||
const form = useForm<FormInputs>({
|
||||
@@ -92,7 +95,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
nsfw_override: null,
|
||||
content_warning_level: ContentWarningLevel.INHERIT,
|
||||
content_warning_text: '',
|
||||
bitrate: 64,
|
||||
bitrate: BITRATE_KBPS_DEFAULT,
|
||||
user_limit: 0,
|
||||
voice_connection_limit: VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT,
|
||||
rtc_region: null,
|
||||
@@ -107,7 +110,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
nsfw_override: channel.nsfwOverride,
|
||||
content_warning_level: channel.contentWarningLevel ?? ContentWarningLevel.INHERIT,
|
||||
content_warning_text: channel.contentWarningText ?? '',
|
||||
bitrate: channel.bitrate ? Math.round(channel.bitrate / 1000) : 64,
|
||||
bitrate: Math.min(channel.bitrate ? Math.round(channel.bitrate / 1000) : BITRATE_KBPS_DEFAULT, maxBitrateKbps),
|
||||
user_limit: channel.userLimit ?? 0,
|
||||
voice_connection_limit: channel.voiceConnectionLimit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT,
|
||||
rtc_region: channel.rtcRegion ?? null,
|
||||
@@ -184,7 +187,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
updateData.rate_limit_per_user = data.slowmode;
|
||||
}
|
||||
if (channel.type === ChannelTypes.GUILD_VOICE) {
|
||||
updateData.bitrate = (data.bitrate ?? 64) * 1000;
|
||||
updateData.bitrate = Math.min(data.bitrate ?? BITRATE_KBPS_DEFAULT, maxBitrateKbps) * 1000;
|
||||
updateData.user_limit = data.user_limit;
|
||||
updateData.voice_connection_limit = data.voice_connection_limit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT;
|
||||
} else if (channel.type === ChannelTypes.GUILD_LINK) {
|
||||
@@ -216,7 +219,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
nsfw_override: data.nsfw_override,
|
||||
content_warning_level: data.content_warning_level,
|
||||
content_warning_text: data.content_warning_text ?? '',
|
||||
bitrate: data.bitrate ?? currentValues.bitrate ?? 64,
|
||||
bitrate: Math.min(data.bitrate ?? currentValues.bitrate ?? BITRATE_KBPS_DEFAULT, maxBitrateKbps),
|
||||
user_limit: data.user_limit ?? currentValues.user_limit ?? 0,
|
||||
voice_connection_limit:
|
||||
data.voice_connection_limit ?? currentValues.voice_connection_limit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT,
|
||||
@@ -224,7 +227,7 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
});
|
||||
ToastCommands.createToast({type: 'success', children: <Trans>Channel updated</Trans>});
|
||||
},
|
||||
[canManageChannel, canUpdateRtcRegion, channel, form, commitRemoteValues],
|
||||
[canManageChannel, canUpdateRtcRegion, channel, form, commitRemoteValues, maxBitrateKbps],
|
||||
);
|
||||
const {handleSubmit: handleSave} = useFormSubmit({
|
||||
form,
|
||||
@@ -309,7 +312,11 @@ const ChannelOverviewTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
{showVoiceSection && (
|
||||
<div className={styles.settingsGroup} data-flx="channel.channel-tabs.channel-overview-tab.settings-group--3">
|
||||
{canManageChannel && (
|
||||
<VoiceSettings form={form} data-flx="channel.channel-tabs.channel-overview-tab.voice-settings" />
|
||||
<VoiceSettings
|
||||
form={form}
|
||||
maxBitrateKbps={maxBitrateKbps}
|
||||
data-flx="channel.channel-tabs.channel-overview-tab.voice-settings"
|
||||
/>
|
||||
)}
|
||||
{canUpdateRtcRegion && (
|
||||
<RtcRegionSelect
|
||||
|
||||
+14
-13
@@ -3,10 +3,10 @@
|
||||
import styles from '@app/features/channel/components/modals/channel_tabs/ChannelOverviewTab.module.css';
|
||||
import {SettingsControlRow} from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/SettingsControlRow';
|
||||
import {
|
||||
BITRATE_KBPS_MARKERS,
|
||||
BITRATE_KBPS_MAX,
|
||||
BITRATE_KBPS_DEFAULT,
|
||||
BITRATE_KBPS_MIN,
|
||||
type FormInputs,
|
||||
getBitrateKbpsMarkers,
|
||||
} from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/shared';
|
||||
import {RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR, Slider} from '@app/features/ui/components/Slider';
|
||||
import {
|
||||
@@ -50,6 +50,7 @@ const CONNECTION_LIMIT_VALUE_DESCRIPTOR = msg({
|
||||
|
||||
interface VoiceSettingsProps {
|
||||
form: UseFormReturn<FormInputs>;
|
||||
maxBitrateKbps: number;
|
||||
}
|
||||
|
||||
const formatIntegerValue = (value: number): string => String(Math.round(value));
|
||||
@@ -66,15 +67,14 @@ const formatConnectionLimitMarker = (value: number): string | null => {
|
||||
|
||||
const VoiceBitrateSlider: React.FC<{
|
||||
value: number | undefined;
|
||||
maxBitrateKbps: number;
|
||||
onChange: (value: number) => void;
|
||||
}> = ({value, onChange}) => {
|
||||
}> = ({value, maxBitrateKbps, onChange}) => {
|
||||
const {i18n} = useLingui();
|
||||
const voiceQualityLabel = i18n._(VOICE_QUALITY_DESCRIPTOR);
|
||||
const resetSliderLabel = i18n._(RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR);
|
||||
let currentValue = value;
|
||||
if (typeof currentValue !== 'number') {
|
||||
currentValue = 64;
|
||||
}
|
||||
const factoryDefaultValue = Math.min(BITRATE_KBPS_DEFAULT, maxBitrateKbps);
|
||||
const currentValue = Math.min(typeof value === 'number' ? value : BITRATE_KBPS_DEFAULT, maxBitrateKbps);
|
||||
return (
|
||||
<SettingsControlRow
|
||||
label={voiceQualityLabel}
|
||||
@@ -88,18 +88,18 @@ const VoiceBitrateSlider: React.FC<{
|
||||
<Slider
|
||||
value={currentValue}
|
||||
defaultValue={currentValue}
|
||||
factoryDefaultValue={64}
|
||||
factoryDefaultValue={factoryDefaultValue}
|
||||
minValue={BITRATE_KBPS_MIN}
|
||||
maxValue={BITRATE_KBPS_MAX}
|
||||
maxValue={maxBitrateKbps}
|
||||
step={1}
|
||||
markers={[...BITRATE_KBPS_MARKERS]}
|
||||
markers={getBitrateKbpsMarkers(maxBitrateKbps)}
|
||||
ariaLabel={voiceQualityLabel}
|
||||
ariaValueText={i18n._(KBPS_DESCRIPTOR, {kilobits: Math.round(currentValue)})}
|
||||
onMarkerRender={formatIntegerValue}
|
||||
onValueRender={(kilobits) => i18n._(KBPS_DESCRIPTOR, {kilobits: Math.round(kilobits)})}
|
||||
onValueChange={(kilobits) => onChange(Math.round(kilobits))}
|
||||
showResetButton={true}
|
||||
onReset={() => onChange(64)}
|
||||
onReset={() => onChange(factoryDefaultValue)}
|
||||
resetTooltip={resetSliderLabel}
|
||||
data-flx="channel.channel-tabs.channel-overview-tab.voice-settings.voice-bitrate-slider.slider"
|
||||
/>
|
||||
@@ -108,7 +108,7 @@ const VoiceBitrateSlider: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const VoiceSettings: React.FC<VoiceSettingsProps> = ({form}) => {
|
||||
export const VoiceSettings: React.FC<VoiceSettingsProps> = ({form, maxBitrateKbps}) => {
|
||||
const {i18n} = useLingui();
|
||||
const resetSliderLabel = i18n._(RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR);
|
||||
const participantLimitLabel = i18n._(PARTICIPANT_LIMIT_LABEL_DESCRIPTOR);
|
||||
@@ -121,6 +121,7 @@ export const VoiceSettings: React.FC<VoiceSettingsProps> = ({form}) => {
|
||||
render={({field}) => (
|
||||
<VoiceBitrateSlider
|
||||
value={field.value}
|
||||
maxBitrateKbps={maxBitrateKbps}
|
||||
onChange={field.onChange}
|
||||
data-flx="channel.channel-tabs.channel-overview-tab.voice-settings.voice-bitrate-slider.change"
|
||||
/>
|
||||
@@ -172,7 +173,7 @@ export const VoiceSettings: React.FC<VoiceSettingsProps> = ({form}) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const VoiceConnectionLimitControl: React.FC<VoiceSettingsProps> = ({form}) => {
|
||||
export const VoiceConnectionLimitControl: React.FC<{form: UseFormReturn<FormInputs>}> = ({form}) => {
|
||||
const {i18n} = useLingui();
|
||||
const resetSliderLabel = i18n._(RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR);
|
||||
const connectionLimitLabel = i18n._(CONNECTION_LIMIT_LABEL_DESCRIPTOR);
|
||||
|
||||
+13
-3
@@ -2,7 +2,8 @@
|
||||
|
||||
import type {ChannelRtcRegion} from '@app/features/channel/commands/ChannelCommands';
|
||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||
import {VOICE_CHANNEL_BITRATE_MAX, VOICE_CHANNEL_BITRATE_MIN} from '@fluxer/constants/src/LimitConstants';
|
||||
import {getMaxVoiceChannelBitrate} from '@fluxer/constants/src/GuildConstants';
|
||||
import {VOICE_CHANNEL_BITRATE_DEFAULT, VOICE_CHANNEL_BITRATE_MIN} from '@fluxer/constants/src/LimitConstants';
|
||||
|
||||
export interface FormInputs {
|
||||
name: string;
|
||||
@@ -20,8 +21,17 @@ export interface FormInputs {
|
||||
|
||||
export const CHANNEL_OVERVIEW_TAB_ID = 'overview';
|
||||
export const BITRATE_KBPS_MIN = VOICE_CHANNEL_BITRATE_MIN / 1000;
|
||||
export const BITRATE_KBPS_MAX = VOICE_CHANNEL_BITRATE_MAX / 1000;
|
||||
export const BITRATE_KBPS_MARKERS: ReadonlyArray<number> = [BITRATE_KBPS_MIN, 64, 128, 256, BITRATE_KBPS_MAX];
|
||||
export const BITRATE_KBPS_DEFAULT = VOICE_CHANNEL_BITRATE_DEFAULT / 1000;
|
||||
|
||||
export function getMaxBitrateKbps(guildFeatures: Iterable<string> | null | undefined): number {
|
||||
return getMaxVoiceChannelBitrate(guildFeatures) / 1000;
|
||||
}
|
||||
|
||||
export function getBitrateKbpsMarkers(maxKbps: number): Array<number> {
|
||||
const markers = [BITRATE_KBPS_MIN, BITRATE_KBPS_DEFAULT, 128, 256].filter((marker) => marker < maxKbps);
|
||||
markers.push(maxKbps);
|
||||
return markers;
|
||||
}
|
||||
export const MAX_TOPIC_LENGTH = 1024;
|
||||
|
||||
export interface RtcRegionOption extends ComboboxOption<string | null> {
|
||||
|
||||
@@ -5,7 +5,10 @@ import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import {selectChannel} from '@app/features/navigation/commands/NavigationCommands';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {ChannelTypes, GUILD_TEXT_BASED_CHANNEL_TYPES} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
VOICE_CHANNEL_BITRATE_DEFAULT,
|
||||
VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT,
|
||||
} from '@fluxer/constants/src/LimitConstants';
|
||||
|
||||
export interface FormInputs {
|
||||
name: string;
|
||||
@@ -52,7 +55,7 @@ export async function createChannel(guildId: string, data: FormInputs, parentId?
|
||||
url: data.url,
|
||||
type: channelType,
|
||||
parent_id: parentId || null,
|
||||
bitrate: channelType === ChannelTypes.GUILD_VOICE ? 64000 : null,
|
||||
bitrate: channelType === ChannelTypes.GUILD_VOICE ? VOICE_CHANNEL_BITRATE_DEFAULT : null,
|
||||
user_limit: channelType === ChannelTypes.GUILD_VOICE ? 0 : null,
|
||||
voice_connection_limit: channelType === ChannelTypes.GUILD_VOICE ? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT : null,
|
||||
});
|
||||
|
||||
@@ -143,6 +143,21 @@ const VERIFIED_GUILD_DESCRIPTOR = msg({
|
||||
message: 'Verified community',
|
||||
comment: 'Audit log community feature label for verified communities.',
|
||||
});
|
||||
const AUDIO_BITRATE_128_KBPS_DESCRIPTOR = msg({
|
||||
message: '128 kbps voice quality',
|
||||
comment:
|
||||
'Audit log community feature label for a raised voice channel bitrate ceiling. kbps means kilobits per second.',
|
||||
});
|
||||
const AUDIO_BITRATE_256_KBPS_DESCRIPTOR = msg({
|
||||
message: '256 kbps voice quality',
|
||||
comment:
|
||||
'Audit log community feature label for a raised voice channel bitrate ceiling. kbps means kilobits per second.',
|
||||
});
|
||||
const AUDIO_BITRATE_384_KBPS_DESCRIPTOR = msg({
|
||||
message: '384 kbps voice quality',
|
||||
comment:
|
||||
'Audit log community feature label for a raised voice channel bitrate ceiling. kbps means kilobits per second.',
|
||||
});
|
||||
const VIP_VOICE_DESCRIPTOR = msg({
|
||||
message: 'VIP voice',
|
||||
comment: 'Audit log community feature label for elevated voice capacity/quality.',
|
||||
@@ -404,6 +419,9 @@ export function getSplashAlignmentLabel(value: unknown, i18n: I18n): string | nu
|
||||
const featureLabelMap: Record<string, MessageDescriptor> = {
|
||||
[GuildFeatures.ANIMATED_ICON]: ANIMATED_ICON_DESCRIPTOR,
|
||||
[GuildFeatures.ANIMATED_BANNER]: ANIMATED_BANNER_DESCRIPTOR,
|
||||
[GuildFeatures.AUDIO_BITRATE_128_KBPS]: AUDIO_BITRATE_128_KBPS_DESCRIPTOR,
|
||||
[GuildFeatures.AUDIO_BITRATE_256_KBPS]: AUDIO_BITRATE_256_KBPS_DESCRIPTOR,
|
||||
[GuildFeatures.AUDIO_BITRATE_384_KBPS]: AUDIO_BITRATE_384_KBPS_DESCRIPTOR,
|
||||
[GuildFeatures.BANNER]: BANNER_DESCRIPTOR,
|
||||
[GuildFeatures.CLONE_EMOJI_DISABLED]: EMOJI_CLONING_DISABLED_DESCRIPTOR,
|
||||
[GuildFeatures.CLONE_STICKER_DISABLED]: STICKER_CLONING_DISABLED_DESCRIPTOR,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
|
||||
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import type {VoiceState} from '@app/features/gateway/types/GatewayVoiceTypes';
|
||||
import Guilds from '@app/features/guild/state/Guilds';
|
||||
import Keybind from '@app/features/input/state/InputKeybind';
|
||||
import {getVoiceContextEntranceSoundScope} from '@app/features/notification/utils/EntranceSoundScopes';
|
||||
import {handleMediaPermissionBlocked} from '@app/features/permissions/system/commands/MacPermissionsModalCommands';
|
||||
@@ -117,6 +118,7 @@ import {
|
||||
getActiveVoiceProcessingMode,
|
||||
resolveVoiceProcessingFromStateForDeviceLabel,
|
||||
} from '@app/features/voice/utils/VoiceProcessingProfile';
|
||||
import {resolveVoiceChannelBitrate} from '@fluxer/constants/src/GuildConstants';
|
||||
import type {
|
||||
VoiceEngineV2AudioControls,
|
||||
VoiceEngineV2AudioMode,
|
||||
@@ -606,7 +608,9 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
|
||||
}
|
||||
|
||||
private getMicrophonePublishOptions(channelId: string | null): TrackPublishOptions | undefined {
|
||||
const channelBitrate = channelId ? Channels.getChannel(channelId)?.bitrate : null;
|
||||
const channel = channelId ? Channels.getChannel(channelId) : null;
|
||||
const guild = channel?.guildId ? Guilds.getGuild(channel.guildId) : null;
|
||||
const channelBitrate = resolveVoiceChannelBitrate(channel?.bitrate, guild?.features);
|
||||
const profile = resolveVoiceProcessingFromStateForDeviceLabel(VoiceSettings, this.resolveActiveInputDeviceLabel());
|
||||
return buildMicrophonePublishOptions(channelBitrate, profile.mode);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {resolveVoiceChannelBitrate} from '@fluxer/constants/src/GuildConstants';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {
|
||||
buildMicrophonePublishOptions,
|
||||
@@ -51,6 +52,15 @@ describe('buildMicrophonePublishOptions', () => {
|
||||
red: true,
|
||||
});
|
||||
});
|
||||
it('pins a call outside a guild to 64 kbps', () => {
|
||||
expect(buildMicrophonePublishOptions(resolveVoiceChannelBitrate(null, null), 'voice')).toEqual({
|
||||
audioPreset: {
|
||||
maxBitrate: 64000,
|
||||
priority: 'high',
|
||||
},
|
||||
red: true,
|
||||
});
|
||||
});
|
||||
it('does not force stereo below the stereo voice threshold', () => {
|
||||
expect(buildMicrophonePublishOptions(96000, 'studio')).toEqual({
|
||||
audioPreset: {
|
||||
|
||||
Reference in New Issue
Block a user