Skip to content

Commit

Permalink
feat(player): audio gain support (#1145)
Browse files Browse the repository at this point in the history
  • Loading branch information
codercms authored Feb 22, 2024
1 parent 1c92d02 commit c3e08c5
Show file tree
Hide file tree
Showing 55 changed files with 1,299 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,21 @@ export { DefaultVideoKeyboardActionDisplay };

function getText() {
const { $state } = useContext(mediaContext),
action = $state.lastKeyboardAction()?.action;
action = $state.lastKeyboardAction()?.action,
audioGain = $state.audioGain() ?? 1;
switch (action) {
case 'toggleMuted':
return $state.muted() ? '0%' : getVolumeText($state.volume());
return $state.muted() ? '0%' : getVolumeText($state.volume(), audioGain);
case 'volumeUp':
case 'volumeDown':
return getVolumeText($state.volume());
return getVolumeText($state.volume(), audioGain);
default:
return '';
}
}

function getVolumeText(volume: number) {
return `${Math.round(volume * 100)}%`;
function getVolumeText(volume: number, gain: number) {
return `${Math.round(volume * gain * 100)}%`;
}

function getIcon(Icons?: DefaultKeyboardActionIcons) {
Expand Down Expand Up @@ -174,7 +175,7 @@ function getStatusText(translations?: Partial<DefaultVideoKeyboardActionDisplayT
case 'volumeDown':
return $state.muted() || $state.volume() === 0
? 'Mute'
: `${Math.round($state.volume() * 100)}% ${i18n(translations, 'Volume')}`;
: `${Math.round($state.volume() * ($state.audioGain() ?? 1) * 100)}% ${i18n(translations, 'Volume')}`;
default:
return null;
}
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/layouts/default/media-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ export interface DefaultLayoutProps<Slots = unknown> extends PrimitivePropsWithR
* only applies to the large video layout.
*/
menuGroup?: 'top' | 'bottom';
/**
* Disable audio boost slider in the settings menu.
*/
noAudioGainSlider: boolean;
/**
* The maximum audio gain to be applied. The default is `300` which represents a `300%` boost.
*/
maxAudioGain: number;
/**
* Whether modal menus should be disabled when the small layout is active. A modal menu is
* a floating panel that floats up from the bottom of the screen (outside of the player). It's
Expand Down Expand Up @@ -131,6 +139,8 @@ export function createDefaultMediaLayout({
hideQualityBitrate = false,
icons,
menuGroup = 'bottom',
noAudioGainSlider = false,
maxAudioGain = 300,
noGestures = false,
noKeyboardActionDisplay = false,
noModal = false,
Expand Down Expand Up @@ -181,6 +191,8 @@ export function createDefaultMediaLayout({
icons: icons,
isSmallLayout,
menuGroup,
maxAudioGain,
noAudioGainSlider,
noGestures,
noKeyboardActionDisplay,
noModal,
Expand Down
125 changes: 116 additions & 9 deletions packages/react/src/components/layouts/default/shared-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { PlayButton } from '../../ui/buttons/play-button';
import { SeekButton } from '../../ui/buttons/seek-button';
import { ChapterTitle } from '../../ui/chapter-title';
import * as Menu from '../../ui/menu';
import * as AudioGainSlider from '../../ui/sliders/audio-gain-slider';
import * as TimeSlider from '../../ui/sliders/time-slider';
import * as VolumeSlider from '../../ui/sliders/volume-slider';
import * as Thumbnail from '../../ui/thumbnail';
Expand Down Expand Up @@ -524,6 +525,8 @@ function DefaultChaptersMenu({ tooltip, placement, portalClass }: DefaultMediaMe
$offset = !isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0,
$RemotionThumbnail = useSignal(RemotionThumbnail);

if (disabled) return null;

const Content = (
<Menu.Content
className="vds-chapters-menu-items vds-menu-items"
Expand Down Expand Up @@ -598,20 +601,36 @@ export { DefaultChaptersMenu };

function DefaultSettingsMenu({ tooltip, placement, portalClass, slots }: DefaultMediaMenuProps) {
const { $state } = useMediaContext(),
{ showMenuDelay, icons: Icons, isSmallLayout, menuGroup, noModal } = useDefaultLayoutContext(),
{
showMenuDelay,
icons: Icons,
isSmallLayout,
menuGroup,
noModal,
playbackRates,
noAudioGainSlider,
} = useDefaultLayoutContext(),
settingsText = useDefaultLayoutWord('Settings'),
$viewType = useMediaState('viewType'),
$offset = !isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0,
// Create as a computed signal to avoid unnecessary re-rendering.
$$hasMenuItems = createComputed(() => {
const { canSetPlaybackRate, canSetQuality, qualities, audioTracks, hasCaptions } = $state;
const {
canSetPlaybackRate,
canSetQuality,
canSetAudioGain,
qualities,
audioTracks,
hasCaptions,
} = $state;
return (
canSetPlaybackRate() ||
(canSetQuality() && qualities().length) ||
audioTracks().length ||
!!(canSetPlaybackRate() && playbackRates?.length) ||
!!(canSetQuality() && qualities().length) ||
!!audioTracks().length ||
(!noAudioGainSlider && canSetAudioGain()) ||
hasCaptions()
);
}),
}, [playbackRates, noAudioGainSlider]),
$hasMenuItems = useSignal($$hasMenuItems);

if (!$hasMenuItems) return null;
Expand Down Expand Up @@ -662,13 +681,92 @@ export { DefaultSettingsMenu };
* -----------------------------------------------------------------------------------------------*/

function DefaultAudioSubmenu() {
const label = useDefaultLayoutWord('Audio'),
$canSetAudioGain = useMediaState('canSetAudioGain'),
$audioTracks = useMediaState('audioTracks'),
{ noAudioGainSlider, icons: Icons } = useDefaultLayoutContext(),
hasGainSlider = $canSetAudioGain && !noAudioGainSlider,
$disabled = !hasGainSlider && !$audioTracks.length;

if ($disabled) return null;

return (
<Menu.Root className="vds-audio-menu vds-menu">
<DefaultSubmenuButton label={label} Icon={Icons.Menu.Audio} />
<Menu.Content className="vds-menu-items">
{hasGainSlider ? <DefaultMenuAudioGainSlider /> : null}
<DefaultAudioTracksSubmenu />
</Menu.Content>
</Menu.Root>
);
}

DefaultAudioSubmenu.displayName = 'DefaultAudioSubmenu';

/* -------------------------------------------------------------------------------------------------
* DefaultMenuAudioGainSlider
* -----------------------------------------------------------------------------------------------*/

function DefaultMenuAudioGainSlider() {
const label = useDefaultLayoutWord('Audio Boost'),
$audioGain = useMediaState('audioGain'),
value = Math.round((($audioGain ?? 1) - 1) * 100) + '%',
{ icons: Icons } = useDefaultLayoutContext();

return (
<div className="vds-menu-item vds-menu-item-slider">
<div className="vds-menu-slider-title">
<span className="vds-menu-slider-label">{label}</span>
<span className="vds-menu-slider-value">{value}</span>
</div>
<div className="vds-menu-slider-group">
<Icons.MuteButton.VolumeLow className="vds-icon" />
<DefaultAudioGainSlider />
<Icons.MuteButton.VolumeHigh className="vds-icon" />
</div>
</div>
);
}

DefaultMenuAudioGainSlider.displayName = 'DefaultMenuAudioGainSlider';

/* -------------------------------------------------------------------------------------------------
* DefaultAudioGainSlider
* -----------------------------------------------------------------------------------------------*/

function DefaultAudioGainSlider() {
const label = useDefaultLayoutWord('Audio Boost'),
{ maxAudioGain } = useDefaultLayoutContext();
return (
<AudioGainSlider.Root
className="vds-audio-gain-slider vds-slider"
aria-label={label}
max={maxAudioGain}
>
<AudioGainSlider.Track className="vds-slider-track" />
<AudioGainSlider.TrackFill className="vds-slider-track-fill vds-slider-track" />
<AudioGainSlider.Thumb className="vds-slider-thumb" />
</AudioGainSlider.Root>
);
}

DefaultAudioGainSlider.displayName = 'DefaultAudioGainSlider';

/* -------------------------------------------------------------------------------------------------
* DefaultAudioTracksSubmenu
* -----------------------------------------------------------------------------------------------*/

function DefaultAudioTracksSubmenu() {
const { icons: Icons } = useDefaultLayoutContext(),
label = useDefaultLayoutWord('Audio'),
label = useDefaultLayoutWord('Audio Track'),
defaultText = useDefaultLayoutWord('Default'),
$track = useMediaState('audioTrack'),
options = useAudioOptions();

if (options.disabled) return null;

return (
<Menu.Root className="vds-audio-menu vds-menu">
<Menu.Root className="vds-audio-track-menu vds-menu">
<DefaultSubmenuButton
label={label}
hint={$track?.label ?? defaultText}
Expand Down Expand Up @@ -697,7 +795,7 @@ function DefaultAudioSubmenu() {
);
}

DefaultAudioSubmenu.displayName = 'DefaultAudioSubmenu';
DefaultAudioTracksSubmenu.displayName = 'DefaultAudioTracksSubmenu';

/* -------------------------------------------------------------------------------------------------
* DefaultSpeedSubmenu
Expand All @@ -712,6 +810,9 @@ function DefaultSpeedSubmenu() {
rates: playbackRates,
}),
hint = options.selectedValue === '1' ? normalText : options.selectedValue + 'x';

if (options.disabled) return null;

return (
<Menu.Root className="vds-speed-menu vds-menu">
<DefaultSubmenuButton
Expand Down Expand Up @@ -758,6 +859,9 @@ function DefaultQualitySubmenu() {
options.selectedValue !== 'auto' && currentQuality
? `${currentQuality}p`
: `${autoText}${currentQuality ? ` (${currentQuality}p)` : ''}`;

if (options.disabled) return null;

return (
<Menu.Root className="vds-quality-menu vds-menu">
<DefaultSubmenuButton
Expand Down Expand Up @@ -803,6 +907,9 @@ function DefaultCaptionSubmenu() {
offText = useDefaultLayoutWord('Off'),
options = useCaptionOptions({ off: offText }),
hint = options.selectedTrack?.label ?? offText;

if (options.disabled) return null;

return (
<Menu.Root className="vds-captions-menu vds-menu">
<DefaultSubmenuButton
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/components/primitives/instances.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AirPlayButton,
AudioGainSlider,
CaptionButton,
Captions,
Controls,
Expand Down Expand Up @@ -63,6 +64,7 @@ export class TooltipContentInstance extends TooltipContent {}
export class SliderInstance extends Slider {}
export class TimeSliderInstance extends TimeSlider {}
export class VolumeSliderInstance extends VolumeSlider {}
export class AudioGainSliderInstance extends AudioGainSlider {}
export class SliderThumbnailInstance extends SliderThumbnail {}
export class SliderValueInstance extends SliderValue {}
export class SliderVideoInstance extends SliderVideo {}
Expand Down
96 changes: 96 additions & 0 deletions packages/react/src/components/ui/sliders/audio-gain-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from 'react';

import {
composeRefs,
createReactComponent,
useSignal,
type ReactElementProps,
} from 'maverick.js/react';

import { AudioGainSliderInstance } from '../../primitives/instances';
import { Primitive } from '../../primitives/nodes';
import { type ValueProps } from './slider';
import { SliderValueBridge } from './slider-value';

/* -------------------------------------------------------------------------------------------------
* AudioGainSlider
* -----------------------------------------------------------------------------------------------*/

const AudioGainSliderBridge = createReactComponent(AudioGainSliderInstance, {
domEventsRegex: /^onMedia/,
});

export interface RootProps extends ReactElementProps<AudioGainSliderInstance> {
asChild?: boolean;
children?: React.ReactNode;
ref?: React.Ref<AudioGainSliderInstance>;
}

/**
* Versatile and user-friendly audio boost control designed for seamless cross-browser and provider
* compatibility and accessibility with ARIA support. It offers a smooth user experience for both
* mouse and touch interactions and is highly customizable in terms of styling. Users can
* effortlessly change the audio gain within the range 0 to 100.
*
* @docs {@link https://www.vidstack.io/docs/player/components/sliders/audio-gain-slider}
* @example
* ```tsx
* <AudioGainSlider.Root>
* <AudioGainSlider.Track>
* <AudioGainSlider.TrackFill />
* </AudioGainSlider.Track>
* <AudioGainSlider.Thumb />
* </AudioGainSlider.Root>
* ```
*/
const Root = React.forwardRef<AudioGainSliderInstance, RootProps>(
({ children, ...props }, forwardRef) => {
return (
<AudioGainSliderBridge {...props} ref={forwardRef}>
{(props) => <Primitive.div {...props}>{children}</Primitive.div>}
</AudioGainSliderBridge>
);
},
);

Root.displayName = 'AudioGainSlider';

/* -------------------------------------------------------------------------------------------------
* SliderValue
* -----------------------------------------------------------------------------------------------*/

/**
* Displays the specific numeric representation of the current or pointer value of the audio gain
* slider. When a user interacts with a slider by moving its thumb along the track, the slider value
* and audio gain updates accordingly.
*
* @docs {@link https://www.vidstack.io/docs/player/components/audio-gain-slider#value}
* @example
* ```tsx
* <AudioGainSlider.Root>
* <AudioGainSlider.Preview>
* <AudioGainSlider.Value />
* </AudioGainSlider.Preview>
* </AudioGainSlider.Root>
* ```
*/
const Value = React.forwardRef<HTMLElement, ValueProps>(({ children, ...props }, forwardRef) => {
return (
<SliderValueBridge {...(props as Omit<ValueProps, 'ref'>)}>
{(props, instance) => {
const $text = useSignal(() => instance.getValueText(), instance);
return (
<Primitive.div {...props} ref={composeRefs(props.ref, forwardRef)}>
{$text}
{children}
</Primitive.div>
);
}}
</SliderValueBridge>
);
});

Value.displayName = 'SliderValue';

export * from './slider';
export { Root, Value };
2 changes: 1 addition & 1 deletion packages/react/src/components/ui/sliders/time-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ ChapterTitle.displayName = 'SliderChapterTitle';
* When a user interacts with a slider by moving its thumb along the track, the slider value
* and current playback time updates accordingly.
*
* @docs {@link https://www.vidstack.io/docs/player/components/time-slider#preview}
* @docs {@link https://www.vidstack.io/docs/player/components/time-slider#value}
* @example
* ```tsx
* <TimeSlider.Root>
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/ui/sliders/volume-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Root.displayName = 'VolumeSlider';
* slider. When a user interacts with a slider by moving its thumb along the track, the slider value
* and volume updates accordingly.
*
* @docs {@link https://www.vidstack.io/docs/player/components/volume-slider#preview}
* @docs {@link https://www.vidstack.io/docs/player/components/volume-slider#value}
* @example
* ```tsx
* <VolumeSlider.Root>
Expand Down
Loading

0 comments on commit c3e08c5

Please sign in to comment.