Skip to content

Commit

Permalink
feat(controls): Add Quality support to Dash controls (#1395)
Browse files Browse the repository at this point in the history
  • Loading branch information
Conrad Chan authored Jun 3, 2021
1 parent 110525a commit 7392977
Show file tree
Hide file tree
Showing 18 changed files with 528 additions and 89 deletions.
11 changes: 11 additions & 0 deletions src/lib/viewers/controls/media/HDBadge.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import '../styles';

.bp-HDBadge {
padding: 2px 2px 2px 3px;
color: $white;
font-weight: bold;
font-size: 8px;
letter-spacing: .1em;
background-color: $bdl-box-blue;
border-radius: 3px;
}
6 changes: 6 additions & 0 deletions src/lib/viewers/controls/media/HDBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import './HDBadge.scss';

export default function HDBadge(): JSX.Element {
return <div className="bp-HDBadge">HD</div>;
}
50 changes: 26 additions & 24 deletions src/lib/viewers/controls/media/MediaSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import React from 'react';
import noop from 'lodash/noop';
import getLanguageName from '../../../lang';
import MediaSettingsMenuAudioTracks, { AudioTrack, Props as AudioTracksProps } from './MediaSettingsAudioTracks';
import MediaSettingsMenuAudioTracks, { addLabels, Props as AudioTracksProps } from './MediaSettingsAudioTracks';
import MediaSettingsMenuAutoplay, { Props as AutoplayProps } from './MediaSettingsMenuAutoplay';
import MediaSettingsMenuQuality, {
getLabel as getQualityLabel,
Props as QualityProps,
} from './MediaSettingsMenuQuality';
import MediaSettingsMenuRate, { Props as RateProps } from './MediaSettingsMenuRate';
import Settings, { Menu } from '../settings';
import Settings, { Menu, Props as SettingsProps } from '../settings';

export type Props = Partial<AudioTracksProps> & AutoplayProps & RateProps & { className?: string };

const generateAudioTrackLabel = (language: string, index: number): string => {
let label = `${__('track')} ${index + 1}`;
if (language !== 'und') {
label = `${label} (${getLanguageName(language) || language})`;
}

return label;
};

const addLabels = (audioTracks: Array<AudioTrack>): Array<AudioTrack> =>
audioTracks.map((track, index) => {
const { language } = track;
const label = generateAudioTrackLabel(language, index);
return {
...track,
label,
};
});
export type Props = Partial<AudioTracksProps> &
Partial<QualityProps> &
Partial<SettingsProps> &
AutoplayProps &
RateProps & { className?: string };

export default function MediaSettings({
audioTrack,
audioTracks = [],
autoplay,
badge,
className,
onAudioTrackChange = noop,
onAutoplayChange,
onQualityChange,
onRateChange,
quality,
rate,
toggle,
}: Props): JSX.Element {
const autoValue = autoplay ? __('media_autoplay_enabled') : __('media_autoplay_disabled');
const rateValue = rate === '1.0' || !rate ? __('media_speed_normal') : rate;
Expand All @@ -45,17 +37,27 @@ export default function MediaSettings({
const showAudioTrackItems = audioTracks.length > 1;

return (
<Settings className={className}>
<Settings badge={badge} className={className} toggle={toggle}>
<Settings.Menu name={Menu.MAIN}>
<Settings.MenuItem label={__('media_autoplay')} target={Menu.AUTOPLAY} value={autoValue} />
<Settings.MenuItem label={__('media_speed')} target={Menu.RATE} value={rateValue} />
{quality && (
<Settings.MenuItem
label={__('media_quality')}
target={Menu.QUALITY}
value={getQualityLabel(quality)}
/>
)}
{showAudioTrackItems && (
<Settings.MenuItem label={__('media_audio')} target={Menu.AUDIO} value={audioTrackLabel} />
)}
</Settings.Menu>

<MediaSettingsMenuAutoplay autoplay={autoplay} onAutoplayChange={onAutoplayChange} />
<MediaSettingsMenuRate onRateChange={onRateChange} rate={rate} />
{quality && onQualityChange && (
<MediaSettingsMenuQuality onQualityChange={onQualityChange} quality={quality} />
)}
{showAudioTrackItems && (
<MediaSettingsMenuAudioTracks
audioTrack={audioTrack}
Expand Down
20 changes: 20 additions & 0 deletions src/lib/viewers/controls/media/MediaSettingsAudioTracks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import getLanguageName from '../../../lang';
import Settings, { Menu } from '../settings';

export type AudioTrack = {
Expand All @@ -14,6 +15,25 @@ export type Props = {
onAudioTrackChange: (id: number) => void;
};

export const generateAudioTrackLabel = (language: string, index: number): string => {
let label = `${__('track')} ${index + 1}`;
if (language !== 'und') {
label = `${label} (${getLanguageName(language) || language})`;
}

return label;
};

export const addLabels = (audioTracks: Array<AudioTrack>): Array<AudioTrack> =>
audioTracks.map((track, index) => {
const { language } = track;
const label = generateAudioTrackLabel(language, index);
return {
...track,
label,
};
});

export default function MediaSettingsMenuAudioTracks({
audioTrack,
audioTracks,
Expand Down
55 changes: 55 additions & 0 deletions src/lib/viewers/controls/media/MediaSettingsMenuQuality.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import noop from 'lodash/noop';
import Settings, { Menu } from '../settings';

export type Props = {
onQualityChange: (quality: Quality) => void;
quality: Quality;
};

export enum Quality {
AUTO = 'auto',
HD = 'hd',
SD = 'sd',
}

const QUALITY_LABEL_MAP: Record<Quality, string> = {
[Quality.AUTO]: __('media_quality_auto') as string,
[Quality.HD]: '1080p',
[Quality.SD]: '480p',
};

export const getLabel = (quality: Quality): string => QUALITY_LABEL_MAP[quality];

export default function MediaSettingsMenuQuality({ onQualityChange = noop, quality }: Props): JSX.Element {
const { setActiveMenu } = React.useContext(Settings.Context);

const handleChange = (value: Quality): void => {
setActiveMenu(Menu.MAIN);
onQualityChange(value);
};

return (
<Settings.Menu name={Menu.QUALITY}>
<Settings.MenuBack label={__('media_quality')} />
<Settings.RadioItem<Quality>
isSelected={quality === Quality.SD}
label="480p"
onChange={handleChange}
value={Quality.SD}
/>
<Settings.RadioItem<Quality>
isSelected={quality === Quality.HD}
label="1080p"
onChange={handleChange}
value={Quality.HD}
/>
<Settings.RadioItem<Quality>
isSelected={quality === Quality.AUTO}
label={__('media_quality_auto')}
onChange={handleChange}
value={Quality.AUTO}
/>
</Settings.Menu>
);
}
35 changes: 30 additions & 5 deletions src/lib/viewers/controls/media/__tests__/MediaSettings-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Settings from '../../settings/Settings';
import SettingsMenu from '../../settings/SettingsMenu';
import SettingsMenuItem from '../../settings/SettingsMenuItem';
import MediaSettingsMenuAudioTracks from '../MediaSettingsAudioTracks';
import MediaSettingsMenuQuality from '../MediaSettingsMenuQuality';

describe('MediaSettings', () => {
const getWrapper = (props = {}): ShallowWrapper =>
Expand All @@ -19,6 +20,8 @@ describe('MediaSettings', () => {
/>,
);

const CustomToggle = (): JSX.Element => <button type="button">custom button</button>;

describe('render', () => {
test('should return a valid wrapper', () => {
const wrapper = getWrapper();
Expand All @@ -28,12 +31,24 @@ describe('MediaSettings', () => {
expect(wrapper.exists(SettingsMenuItem)).toBe(true);
});

test('should pass optional props to Settings', () => {
const badge = <div className="custom-badge">custom</div>;
const wrapper = getWrapper({ badge, toggle: CustomToggle });
const settings = wrapper.find(Settings);

expect(settings.prop('badge')).toEqual(badge);
expect(settings.prop('toggle')).toEqual(CustomToggle);
});

test.each`
menuItem | value | displayValue
${'autoplay'} | ${true} | ${'Enabled'}
${'autoplay'} | ${false} | ${'Disabled'}
${'rate'} | ${'1.0'} | ${'Normal'}
${'rate'} | ${'2.0'} | ${'2.0'}
menuItem | value | displayValue
${'autoplay'} | ${true} | ${__('media_autoplay_enabled')}
${'autoplay'} | ${false} | ${__('media_autoplay_disabled')}
${'rate'} | ${'1.0'} | ${__('media_speed_normal')}
${'rate'} | ${'2.0'} | ${'2.0'}
${'quality'} | ${'auto'} | ${__('media_quality_auto')}
${'quality'} | ${'sd'} | ${'480p'}
${'quality'} | ${'hd'} | ${'1080p'}
`('should display $displayValue for the $menuItem value $value', ({ displayValue, menuItem, value }) => {
const wrapper = getWrapper({ [menuItem]: value });

Expand Down Expand Up @@ -64,5 +79,15 @@ describe('MediaSettings', () => {
const expectedLabel = `${__('track')} 2 (English)`;
expect(wrapper.find({ target: 'audio' }).prop('value')).toBe(expectedLabel);
});

test('should not render the quality menu item if no quality is provided', () => {
const wrapper = getWrapper();
expect(wrapper.exists(MediaSettingsMenuQuality)).toBe(false);
});

test('should render the quality menu if the quality is provided', () => {
const wrapper = getWrapper({ quality: 'auto', onQualityChange: jest.fn() });
expect(wrapper.exists(MediaSettingsMenuQuality)).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import MediaSettingsMenuQuality, { Quality } from '../MediaSettingsMenuQuality';
import Settings, { Context, Menu } from '../../settings';

describe('MediaSettingsMenuQuality', () => {
const getContext = (): Partial<Context> => ({ setActiveMenu: jest.fn() });
const getWrapper = (props = {}, context = getContext()): ReactWrapper =>
mount(<MediaSettingsMenuQuality onQualityChange={jest.fn()} quality={Quality.AUTO} {...props} />, {
wrappingComponent: Settings.Context.Provider,
wrappingComponentProps: { value: context },
});

describe('event handlers', () => {
test('should surface the selected item on change', () => {
const onQualityChange = jest.fn();
const wrapper = getWrapper({ onQualityChange });

wrapper.find({ value: 'sd' }).simulate('click');

expect(onQualityChange).toBeCalledWith('sd');
});

test('should reset the active menu on change', () => {
const context = getContext();
const wrapper = getWrapper({}, context);

wrapper.find({ value: 'sd' }).simulate('click');

expect(context.setActiveMenu).toBeCalledWith(Menu.MAIN);
});
});

describe('render', () => {
test('should return a valid wrapper', () => {
const wrapper = getWrapper();
const radioItems = wrapper.find(Settings.RadioItem);

expect(wrapper.exists(Settings.MenuBack)).toBe(true);
expect(radioItems.length).toBe(3);
expect(radioItems.at(2).prop('isSelected')).toBe(true);
});
});
});
2 changes: 1 addition & 1 deletion src/lib/viewers/controls/media/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ $bp-MediaControl-width: 36px;
right: 15px;
}

.bp-SettingsToggle {
.bp-SettingsToggle-button {
width: $bp-MediaControl-width;
height: $bp-MediaControl-height;
}
Expand Down
24 changes: 10 additions & 14 deletions src/lib/viewers/controls/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import SettingsCheckboxItem from './SettingsCheckboxItem';
import SettingsContext, { Menu, Rect } from './SettingsContext';
import SettingsDropdown from './SettingsDropdown';
import SettingsFlyout from './SettingsFlyout';
import SettingsGearToggle, { Ref as SettingsToggleRef } from './SettingsToggle';
import SettingsGearToggle, { Props as SettingsGearToggleProps, Ref as SettingsToggleRef } from './SettingsToggle';
import SettingsMenu from './SettingsMenu';
import SettingsMenuBack from './SettingsMenuBack';
import SettingsMenuItem from './SettingsMenuItem';
import SettingsRadioItem from './SettingsRadioItem';
import useClickOutside from '../hooks/useClickOutside';
import { decodeKeydown } from '../../../util';

export type Props = React.PropsWithChildren<{
className?: string;
onClose?: () => void;
onOpen?: () => void;
toggle?: React.ElementType;
}>;

export type Props = Pick<SettingsGearToggleProps, 'badge'> &
React.PropsWithChildren<{
className?: string;
onClose?: () => void;
onOpen?: () => void;
toggle?: React.ElementType;
}>;
export default function Settings({
badge,
children,
className,
onClose = noop,
Expand Down Expand Up @@ -86,12 +87,7 @@ export default function Settings({
{...rest}
>
<SettingsContext.Provider value={{ activeMenu, setActiveMenu, setActiveRect }}>
<SettingsToggle
ref={buttonElRef}
className="bp-Settings-toggle"
isOpen={isOpen}
onClick={handleClick}
/>
<SettingsToggle ref={buttonElRef} badge={badge} isOpen={isOpen} onClick={handleClick} />
<SettingsFlyout className="bp-Settings-flyout" height={height} isOpen={isOpen} width={width}>
{children}
</SettingsFlyout>
Expand Down
15 changes: 14 additions & 1 deletion src/lib/viewers/controls/settings/SettingsToggle.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import '../styles';

.bp-SettingsToggle {
@include bp-Control;
position: relative;

&.bp-is-open {
.bp-SettingsToggle-icon {
Expand All @@ -10,7 +10,20 @@
}
}

.bp-SettingsToggle-button {
@include bp-Control;
}

.bp-SettingsToggle-icon {
transform: rotate(0);
transition: transform 300ms ease;
}

.bp-SettingsToggle-badge {
position: absolute;
top: 50%;
left: 50%;
display: flex;
align-items: center;
transform: translateY(-100%);
}
Loading

0 comments on commit 7392977

Please sign in to comment.