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(controls): Add Subtitles menu for Dash #1400

Merged
merged 6 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,5 @@ export const ERROR_CODE_403_FORBIDDEN_BY_POLICY = 'forbidden_by_policy';
// LocalStorage Keys
export const DOCUMENT_FTUX_CURSOR_SEEN_KEY = 'ftux-cursor-seen-document';
export const IMAGE_FTUX_CURSOR_SEEN_KEY = 'ftux-cursor-seen-image';

export const SUBTITLES_OFF = -1;
32 changes: 22 additions & 10 deletions src/lib/viewers/controls/media/MediaSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import MediaSettingsMenuQuality, {
Props as QualityProps,
} from './MediaSettingsMenuQuality';
import MediaSettingsMenuRate, { Props as RateProps } from './MediaSettingsMenuRate';
import MediaSettingsMenuSubtitles, { getDisplayLanguage, Props as SubtitlesProps } from './MediaSettingsMenuSubtitles';
import Settings, { Menu, Props as SettingsProps } from '../settings';

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

Expand All @@ -25,16 +27,21 @@ export default function MediaSettings({
onAutoplayChange,
onQualityChange,
onRateChange,
onSubtitleChange,
quality,
rate,
subtitle,
subtitles = [],
toggle,
}: Props): JSX.Element {
const subtitleDisplayLanguage = getDisplayLanguage(subtitle, subtitles);
const autoValue = autoplay ? __('media_autoplay_enabled') : __('media_autoplay_disabled');
const rateValue = rate === '1.0' || !rate ? __('media_speed_normal') : rate;
const labelledAudioTracks = React.useMemo(() => addLabels(audioTracks), [audioTracks]);
const hydratedSelectedAudioTrack = labelledAudioTracks.find(({ id }) => audioTrack === id);
const audioTrackLabel = hydratedSelectedAudioTrack ? hydratedSelectedAudioTrack.label : '';
const showAudioTrackItems = audioTracks.length > 1;
const showSubtitles = subtitles.length > 0 && onSubtitleChange;

return (
<Settings badge={badge} className={className} toggle={toggle}>
Expand All @@ -59,6 +66,14 @@ export default function MediaSettings({
value={getQualityLabel(quality)}
/>
)}
{showSubtitles && (
<Settings.MenuItem
data-testid="bp-media-settings-subtitles"
label={`${__('subtitles')}/CC`}
target={Menu.SUBTITLES}
value={subtitleDisplayLanguage}
/>
)}
{showAudioTrackItems && (
<Settings.MenuItem
data-testid="bp-media-settings-audiotracks"
Expand All @@ -71,16 +86,13 @@ export default function MediaSettings({

<MediaSettingsMenuAutoplay autoplay={autoplay} onAutoplayChange={onAutoplayChange} />
<MediaSettingsMenuRate onRateChange={onRateChange} rate={rate} />
{quality && onQualityChange && (
<MediaSettingsMenuQuality onQualityChange={onQualityChange} quality={quality} />
)}
{showAudioTrackItems && (
<MediaSettingsMenuAudioTracks
audioTrack={audioTrack}
audioTracks={labelledAudioTracks}
onAudioTrackChange={onAudioTrackChange}
/>
)}
<MediaSettingsMenuQuality onQualityChange={onQualityChange} quality={quality} />
<MediaSettingsMenuSubtitles onSubtitleChange={onSubtitleChange} subtitle={subtitle} subtitles={subtitles} />
<MediaSettingsMenuAudioTracks
audioTrack={audioTrack}
audioTracks={labelledAudioTracks}
onAudioTrackChange={onAudioTrackChange}
/>
</Settings>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export default function MediaSettingsMenuAudioTracks({
audioTrack,
audioTracks,
onAudioTrackChange,
}: Props): JSX.Element {
}: Props): JSX.Element | null {
const { setActiveMenu } = React.useContext(Settings.Context);

if (audioTracks.length <= 1) {
return null;
}

const handleChange = (value: number): void => {
setActiveMenu(Menu.MAIN);
onAudioTrackChange(value);
Expand Down
11 changes: 7 additions & 4 deletions src/lib/viewers/controls/media/MediaSettingsMenuQuality.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import noop from 'lodash/noop';
import Settings, { Menu } from '../settings';

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

export enum Quality {
Expand All @@ -21,9 +20,13 @@ const QUALITY_LABEL_MAP: Record<Quality, string> = {

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

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

if (!quality || !onQualityChange) {
return null;
}

const handleChange = (value: Quality): void => {
setActiveMenu(Menu.MAIN);
onQualityChange(value);
Expand Down
60 changes: 60 additions & 0 deletions src/lib/viewers/controls/media/MediaSettingsMenuSubtitles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import Settings, { Menu } from '../settings';
import { SUBTITLES_OFF } from '../../../constants';

export type Subtitle = {
displayLanguage: string;
id: number;
};

export type Props = {
onSubtitleChange?: (id: number) => void;
subtitle?: number;
subtitles?: Array<Subtitle>;
};

export const getDisplayLanguage = (subtitle?: number, subtitles: Array<Subtitle> = []): string => {
const { displayLanguage } = subtitles.find(({ id }) => subtitle === id) || {
displayLanguage: __('off'),
};

return displayLanguage;
};

export default function MediaSettingsMenuSubtitles({
onSubtitleChange,
subtitle,
subtitles = [],
}: Props): JSX.Element | null {
const { setActiveMenu } = React.useContext(Settings.Context);

if (!subtitles.length || !onSubtitleChange) {
return null;
}

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

return (
<Settings.Menu name={Menu.SUBTITLES}>
<Settings.MenuBack label={`${__('subtitles')}/CC`} />
<Settings.RadioItem
isSelected={subtitle === SUBTITLES_OFF}
label={__('off')}
onChange={handleChange}
value={SUBTITLES_OFF}
/>
{subtitles.map(({ displayLanguage, id }) => (
<Settings.RadioItem
key={id}
isSelected={subtitle === id}
label={displayLanguage}
onChange={handleChange}
value={id}
/>
))}
</Settings.Menu>
);
}
26 changes: 26 additions & 0 deletions src/lib/viewers/controls/media/SubtitlesToggle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import './styles';

.bp-SubtitlesToggle {
@include bp-MediaButton;

&[aria-pressed='true'] {
.bp-SubtitlesToggle-text {
color: $white;
background-color: $box-blue;
}
}
}

.bp-SubtitlesToggle-text {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $white;
font-weight: bold;
font-size: 12px;
letter-spacing: .1em;
background-color: rgba($black, .3); // Anything less than .3 looks too transparent on IE/Edge
border-radius: 4px;
}
33 changes: 33 additions & 0 deletions src/lib/viewers/controls/media/SubtitlesToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import noop from 'lodash/noop';
import MediaToggle from './MediaToggle';
import { Subtitle } from './MediaSettingsMenuSubtitles';
import './SubtitlesToggle.scss';

export type Props = {
isShowingSubtitles?: boolean;
onSubtitlesToggle?: (isShowingSubtitles: boolean) => void;
subtitles?: Array<Subtitle>;
};

export default function SubtitlesToggle({
isShowingSubtitles,
onSubtitlesToggle = noop,
subtitles = [],
}: Props): JSX.Element | null {
if (!subtitles.length) {
return null;
}

return (
<MediaToggle
aria-pressed={isShowingSubtitles}
className="bp-SubtitlesToggle"
data-testid="bp-media-cc-button"
onClick={(): void => onSubtitlesToggle(!isShowingSubtitles)}
title={__('media_subtitles_cc')}
>
<div className="bp-SubtitlesToggle-text">CC</div>
</MediaToggle>
);
}
6 changes: 6 additions & 0 deletions src/lib/viewers/controls/media/__mocks__/subtitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const subtitles = [
{ id: 0, displayLanguage: 'English' },
{ id: 1, displayLanguage: 'Spanish' },
];

export default subtitles;
63 changes: 37 additions & 26 deletions src/lib/viewers/controls/media/__tests__/MediaSettings-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import MediaSettings from '../MediaSettings';
import Settings from '../../settings/Settings';
import SettingsMenu from '../../settings/SettingsMenu';
import SettingsMenuItem from '../../settings/SettingsMenuItem';
import subtitles from '../__mocks__/subtitles';
import MediaSettingsMenuAudioTracks from '../MediaSettingsAudioTracks';
import MediaSettingsMenuQuality from '../MediaSettingsMenuQuality';
import MediaSettingsMenuSubtitles from '../MediaSettingsMenuSubtitles';

describe('MediaSettings', () => {
const getWrapper = (props = {}): ShallowWrapper =>
Expand Down Expand Up @@ -55,39 +57,48 @@ describe('MediaSettings', () => {
expect(wrapper.find({ target: menuItem }).prop('value')).toBe(displayValue);
});

test('should not render the audio menu item if no audio tracks are provided', () => {
const wrapper = getWrapper();
expect(wrapper.exists({ target: 'audio' })).toBe(false);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(false);
});
describe('audiotracks menu', () => {
test('should render the audio menu if > 1 audio tracks are present', () => {
const wrapper = getWrapper({ audioTracks });
expect(wrapper.exists({ target: 'audio' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(true);
});

test('should not render the audio menu item if only 1 audio track is present', () => {
const singleAudioTrack = [{ id: 0, language: 'und' }];
const wrapper = getWrapper({ audioTracks: singleAudioTrack });
expect(wrapper.exists({ target: 'audio' })).toBe(false);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(false);
test('should display the generated track label for the selected audio track', () => {
const wrapper = getWrapper({ audioTrack: 1, audioTracks });
const expectedLabel = `${__('track')} 2 (English)`;
expect(wrapper.find({ target: 'audio' }).prop('value')).toBe(expectedLabel);
});
});

test('should render the audio menu if > 1 audio tracks are present', () => {
const wrapper = getWrapper({ audioTracks });
expect(wrapper.exists({ target: 'audio' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuAudioTracks)).toBe(true);
describe('quality menu', () => {
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);
});
});

test('should display the generated track label for the selected audio track', () => {
const wrapper = getWrapper({ audioTrack: 1, audioTracks });
const expectedLabel = `${__('track')} 2 (English)`;
expect(wrapper.find({ target: 'audio' }).prop('value')).toBe(expectedLabel);
});
describe('subtitles menu', () => {
test('should render the subtitles menu item if only 1 subtitles track is present', () => {
const onSubtitleChange = jest.fn();
const singleSubtitle = [{ id: 0, displayLanguage: 'English' }];
const wrapper = getWrapper({ onSubtitleChange, subtitles: singleSubtitle });
expect(wrapper.exists({ target: 'subtitles' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuSubtitles)).toBe(true);
});

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 subtitle menu if > 1 subtitles are present', () => {
const onSubtitleChange = jest.fn();
const wrapper = getWrapper({ onSubtitleChange, subtitles });
expect(wrapper.exists({ target: 'subtitles' })).toBe(true);
expect(wrapper.exists(MediaSettingsMenuSubtitles)).toBe(true);
});

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);
test('should display the subtitle language for the selected audio track', () => {
const onSubtitleChange = jest.fn();
const wrapper = getWrapper({ onSubtitleChange, subtitle: 1, subtitles });
expect(wrapper.find({ target: 'subtitles' }).prop('value')).toBe('Spanish');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ describe('MediaSettingsAudioTracks', () => {
});

describe('render', () => {
test('should not render if audiotracks is <= 1', () => {
const wrapper = getWrapper({ audioTracks: [] });

expect(wrapper.isEmptyRender()).toBe(true);
});

test('should return a valid wrapper', () => {
const wrapper = getWrapper();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ describe('MediaSettingsMenuQuality', () => {
});

describe('render', () => {
test('should not render if no quality is provided', () => {
const wrapper = getWrapper({ quality: undefined });

expect(wrapper.isEmptyRender()).toBe(true);
});

test('should not render if no callback is provided', () => {
const wrapper = getWrapper({ onQualityChange: undefined });

expect(wrapper.isEmptyRender()).toBe(true);
});

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