Skip to content

Commit

Permalink
Merge pull request #1356 from Pompette/feature/responsive_volume_comp…
Browse files Browse the repository at this point in the history
…onent
  • Loading branch information
JonnyBurger committed Oct 4, 2022
2 parents 027c502 + 81336d0 commit cbc5186
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 74 deletions.
154 changes: 93 additions & 61 deletions packages/player/src/MediaVolumeSlider.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
};

1 comment on commit cbc5186

@vercel
Copy link

@vercel vercel bot commented on cbc5186 Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.