Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make volume slider responsive according to player size #1356

Merged
154 changes: 93 additions & 61 deletions packages/player/src/MediaVolumeSlider.tsx
@@ -1,74 +1,24 @@
import React, {useCallback, useRef, useState} from 'react';
import {Internals} from 'remotion';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Internals, random} from 'remotion';
import {ICON_SIZE, VolumeOffIcon, VolumeOnIcon} from './icons';
import {VOLUME_SLIDER_INPUT_CSS_CLASSNAME} from './player-css-classname';
import {useHoverState} from './use-hover-state';

const BAR_HEIGHT = 5;
const KNOB_SIZE = 12;
const VOLUME_SLIDER_WIDTH = 100;
export const VOLUME_SLIDER_WIDTH = 100;

const scope = `.${VOLUME_SLIDER_INPUT_CSS_CLASSNAME}`;
const sliderStyle = `
${scope} {
-webkit-appearance: none;
background-color: rgba(255, 255, 255, 0.5);
border-radius: ${BAR_HEIGHT / 2}px;
cursor: pointer;
height: ${BAR_HEIGHT}px;
margin-left: 5px;
width: ${VOLUME_SLIDER_WIDTH}px;
}

${scope}::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: white;
border-radius: ${KNOB_SIZE / 2}px;
box-shadow: 0 0 2px black;
height: ${KNOB_SIZE}px;
width: ${KNOB_SIZE}px;
}

${scope}::-moz-range-thumb {
-webkit-appearance: none;
background-color: white;
border-radius: ${KNOB_SIZE / 2}px;
box-shadow: 0 0 2px black;
height: ${KNOB_SIZE}px;
width: ${KNOB_SIZE}px;
}
`;

Internals.CSSUtils.injectCSS(sliderStyle);

const parentDivStyle: React.CSSProperties = {
display: 'inline-flex',
background: 'none',
border: 'none',
padding: '6px',
justifyContent: 'center',
alignItems: 'center',
touchAction: 'none',
};

const volumeContainer: React.CSSProperties = {
display: 'inline',
width: ICON_SIZE,
height: ICON_SIZE,
cursor: 'pointer',
appearance: 'none',
background: 'none',
border: 'none',
padding: 0,
};

export const MediaVolumeSlider: React.FC = () => {
export const MediaVolumeSlider: React.FC<{
displayVerticalVolumeSlider: Boolean;
}> = ({displayVerticalVolumeSlider}) => {
const [mediaMuted, setMediaMuted] = Internals.useMediaMutedState();
const [mediaVolume, setMediaVolume] = Internals.useMediaVolumeState();
const [focused, setFocused] = useState<boolean>(false);
const parentDivRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const hover = useHoverState(parentDivRef);
const [randomClass] = useState(() =>
`slider-${random(null)}`.replace('.', '')
);
const isMutedOrZero = mediaMuted || mediaVolume === 0;

const onVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -95,8 +45,90 @@ export const MediaVolumeSlider: React.FC = () => {
setMediaMuted((mute) => !mute);
}, [mediaVolume, setMediaMuted, setMediaVolume]);

const parentDivStyle: React.CSSProperties = useMemo(() => {
return {
display: 'inline-flex',
background: 'none',
border: 'none',
padding: '6px',
justifyContent: 'center',
alignItems: 'center',
touchAction: 'none',
...(displayVerticalVolumeSlider && {position: 'relative' as const}),
};
}, [displayVerticalVolumeSlider]);

const volumeContainer: React.CSSProperties = useMemo(() => {
return {
display: 'inline',
width: ICON_SIZE,
height: ICON_SIZE,
cursor: 'pointer',
appearance: 'none',
background: 'none',
border: 'none',
padding: 0,
};
}, []);

const inputStyle = useMemo((): React.CSSProperties => {
const commonStyle: React.CSSProperties = {
WebkitAppearance: 'none',
backgroundColor: 'rgba(255, 255, 255, 0.5)',
borderRadius: BAR_HEIGHT / 2,
cursor: 'pointer',
height: BAR_HEIGHT,
width: VOLUME_SLIDER_WIDTH,
};
if (displayVerticalVolumeSlider) {
return {
...commonStyle,
transform: `rotate(-90deg)`,
position: 'absolute',
bottom: ICON_SIZE + VOLUME_SLIDER_WIDTH / 2 + 5,
};
}

return {
...commonStyle,
marginLeft: 5,
};
}, [displayVerticalVolumeSlider]);

const sliderStyle = `
.${randomClass}::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: white;
border-radius: ${KNOB_SIZE / 2}px;
box-shadow: 0 0 2px black;
height: ${KNOB_SIZE}px;
width: ${KNOB_SIZE}px;
}
.${randomClass} {
background-image: linear-gradient(
to right,
white ${mediaVolume * 100}%, rgba(255, 255, 255, 0) ${mediaVolume * 100}%
);
}

.${randomClass}::-moz-range-thumb {
-webkit-appearance: none;
background-color: white;
border-radius: ${KNOB_SIZE / 2}px;
box-shadow: 0 0 2px black;
height: ${KNOB_SIZE}px;
width: ${KNOB_SIZE}px;
}
`;

return (
<div ref={parentDivRef} style={parentDivStyle}>
<style
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sliderStyle,
}}
/>
<button
aria-label={isMutedOrZero ? 'Unmute sound' : 'Mute sound'}
title={isMutedOrZero ? 'Unmute sound' : 'Mute sound'}
Expand All @@ -108,19 +140,19 @@ export const MediaVolumeSlider: React.FC = () => {
>
{isMutedOrZero ? <VolumeOffIcon /> : <VolumeOnIcon />}
</button>

{(focused || hover) && !mediaMuted ? (
<input
ref={inputRef}
aria-label="Change volume"
className={VOLUME_SLIDER_INPUT_CSS_CLASSNAME}
className={randomClass}
max={1}
min={0}
onBlur={() => setFocused(false)}
onChange={onVolumeChange}
step={0.01}
type="range"
value={mediaVolume}
style={inputStyle}
/>
) : null}
</div>
Expand Down
34 changes: 25 additions & 9 deletions packages/player/src/PlayerControls.tsx
Expand Up @@ -6,6 +6,10 @@ import {FullscreenIcon, PauseIcon, PlayIcon} from './icons';
import {MediaVolumeSlider} from './MediaVolumeSlider';
import {PlayerSeekBar} from './PlayerSeekBar';
import type {usePlayer} from './use-player';
import {useVideoControlsResize} from './use-video-controls-resize';

export const X_SPACER = 10;
export const X_PADDING = 12;

const containerStyle: React.CSSProperties = {
boxSizing: 'border-box',
Expand All @@ -16,8 +20,8 @@ const containerStyle: React.CSSProperties = {
paddingBottom: 10,
background: 'linear-gradient(transparent, rgba(0, 0, 0, 0.4))',
display: 'flex',
paddingRight: 12,
paddingLeft: 12,
paddingRight: X_PADDING,
paddingLeft: X_PADDING,
flexDirection: 'column',
transition: 'opacity 0.3s',
};
Expand Down Expand Up @@ -64,12 +68,6 @@ const flex1: React.CSSProperties = {

const fullscreen: React.CSSProperties = {};

const timeLabel: React.CSSProperties = {
color: 'white',
fontFamily: 'sans-serif',
fontSize: 14,
};

declare global {
interface Document {
webkitFullscreenEnabled?: boolean;
Expand Down Expand Up @@ -97,6 +95,7 @@ export const Controls: React.FC<{
inFrame: number | null;
outFrame: number | null;
initiallyShowControls: number | boolean;
playerWidth: number;
}> = ({
durationInFrames,
hovered,
Expand All @@ -113,10 +112,14 @@ export const Controls: React.FC<{
inFrame,
outFrame,
initiallyShowControls,
playerWidth,
}) => {
const playButtonRef = useRef<HTMLButtonElement | null>(null);
const frame = Internals.Timeline.useTimelinePosition();
const [supportsFullscreen, setSupportsFullscreen] = useState(false);

const {maxTimeLabelWidth, displayVerticalVolumeSlider} =
useVideoControlsResize({allowFullscreen, playerWidth});
const [shouldShowInitially, setInitiallyShowControls] = useState<
boolean | number
>(() => {
Expand Down Expand Up @@ -191,6 +194,17 @@ export const Controls: React.FC<{
};
}, [shouldShowInitially]);

const timeLabel: React.CSSProperties = useMemo(() => {
return {
color: 'white',
fontFamily: 'sans-serif',
fontSize: 14,
maxWidth: maxTimeLabelWidth,
overflow: 'hidden',
textOverflow: 'ellipsis',
};
}, [maxTimeLabelWidth]);

return (
<div style={containerCss}>
<div style={controlsRow}>
Expand All @@ -208,7 +222,9 @@ export const Controls: React.FC<{
{showVolumeControls ? (
<>
<div style={xSpacer} />
<MediaVolumeSlider />
<MediaVolumeSlider
displayVerticalVolumeSlider={displayVerticalVolumeSlider}
/>
</>
) : null}
<div style={xSpacer} />
Expand Down
1 change: 1 addition & 0 deletions packages/player/src/PlayerUI.tsx
Expand Up @@ -517,6 +517,7 @@ const PlayerUI: React.ForwardRefRenderFunction<
inFrame={inFrame}
outFrame={outFrame}
initiallyShowControls={initiallyShowControls}
playerWidth={canvasSize?.width ?? 0}
/>
) : null}
</>
Expand Down
2 changes: 1 addition & 1 deletion packages/player/src/icons.tsx
@@ -1,7 +1,7 @@
import React from 'react';

export const ICON_SIZE = 25;
const fullscreenIconSize = 16;
export const fullscreenIconSize = 16;

const rotate: React.CSSProperties = {
transform: `rotate(90deg)`,
Expand Down
3 changes: 0 additions & 3 deletions packages/player/src/player-css-classname.ts
@@ -1,4 +1 @@
export const PLAYER_CSS_CLASSNAME = '__remotion-player';
export const VOLUME_SLIDER_INPUT_CSS_CLASSNAME = PLAYER_CSS_CLASSNAME.concat(
'_volume-slider-input'
);
60 changes: 60 additions & 0 deletions packages/player/src/use-video-controls-resize.ts
@@ -0,0 +1,60 @@
import {useMemo} from 'react';
import {fullscreenIconSize, ICON_SIZE} from './icons';
import {VOLUME_SLIDER_WIDTH} from './MediaVolumeSlider';
import {X_PADDING, X_SPACER} from './PlayerControls';

export const useVideoControlsResize = ({
allowFullscreen: allowFullScreen,
playerWidth,
}: {
allowFullscreen: boolean;
playerWidth: number;
}): {
maxTimeLabelWidth: number;
displayVerticalVolumeSlider: boolean;
} => {
const resizeInfo = useMemo((): {
maxTimeLabelWidth: number;
displayVerticalVolumeSlider: boolean;
} => {
const playPauseIconSize = ICON_SIZE;
const volumeIconSize = ICON_SIZE;
const _fullscreenIconSize = allowFullScreen ? fullscreenIconSize : 0;
const elementsSize =
volumeIconSize +
playPauseIconSize +
_fullscreenIconSize +
X_PADDING * 2 +
X_SPACER * 2;

const maxTimeLabelWidth = playerWidth - elementsSize;

const maxTimeLabelWidthWithoutNegativeValue = Math.max(
maxTimeLabelWidth,
0
);

const availableTimeLabelWidthIfVolumeOpen =
maxTimeLabelWidthWithoutNegativeValue - VOLUME_SLIDER_WIDTH;

// If max label width is lower than the volume width
// then it means we need to take it's width as the max label width
// otherwise we took the available width when volume open
const computedLabelWidth =
availableTimeLabelWidthIfVolumeOpen < VOLUME_SLIDER_WIDTH
? maxTimeLabelWidthWithoutNegativeValue
: availableTimeLabelWidthIfVolumeOpen;
const minWidthForHorizontalDisplay =
computedLabelWidth + elementsSize + VOLUME_SLIDER_WIDTH;

const displayVerticalVolumeSlider =
playerWidth < minWidthForHorizontalDisplay;

return {
maxTimeLabelWidth: maxTimeLabelWidthWithoutNegativeValue,
displayVerticalVolumeSlider,
};
}, [allowFullScreen, playerWidth]);

return resizeInfo;
};