Skip to content

Commit 26b5b0d

Browse files
committed
Subtitles/Captions control
1 parent 3d3765f commit 26b5b0d

File tree

8 files changed

+192
-26
lines changed

8 files changed

+192
-26
lines changed
Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,59 @@
11
.component {
22
position: relative;
3-
color: #fff;
43
}
54

6-
.button {}
5+
.component:hover {
6+
background-color: #000;
7+
}
8+
9+
.button {
10+
width: 34px;
11+
height: 34px;
12+
background: none;
13+
border: 0;
14+
color: inherit;
15+
font: inherit;
16+
line-height: normal;
17+
overflow: visible;
18+
padding: 0;
19+
cursor: pointer;
20+
}
21+
22+
.button:focus {
23+
outline: 0;
24+
}
25+
26+
.icon {
27+
padding: 5px;
28+
}
729

830
.trackList {
931
position: absolute;
10-
left: 0;
32+
right: 0;
1133
bottom: 100%;
1234
display: none;
13-
background: #000;
35+
background-color: rgba(0,0,0,0.7);
1436
list-style: none;
1537
padding: 0;
1638
margin: 0;
39+
color: #fff;
1740
}
1841

1942
.component:hover .trackList {
2043
display: block;
2144
}
2245

2346
.trackItem {
24-
padding: 5px;
47+
padding: 7px;
48+
cursor: pointer;
49+
}
50+
51+
.activeTrackItem,
52+
.trackItem:hover {
53+
background: #000;
2554
}
2655

27-
.activeTrack {
56+
.activeTrackItem {
2857
composes: trackItem;
2958
text-decoration: underline;
3059
}

src/DefaultPlayer/Captions/Captions.js

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
import React from 'react';
22
import styles from './Captions.css';
3+
import ClosedCaptionIcon from './../Icon/closed_caption.svg';
34

4-
export default ({ textTracks, getVideoEl, forceUpdateState, className }) => {
5+
export default ({ textTracks, onClick, onItemClick, className }) => {
56
return (
67
<div className={[
78
styles.component,
89
className
910
].join(' ')}>
10-
<button className={styles.button}>
11-
CC
11+
<button
12+
type="button"
13+
onClick={onClick}
14+
aria-label="Captions"
15+
className={styles.button}>
16+
<ClosedCaptionIcon
17+
className={styles.icon}
18+
fill="#fff" />
1219
</button>
1320
<ul className={styles.trackList}>
14-
{ textTracks && Array.prototype.slice.call(textTracks).map((track, i) => (
21+
{ textTracks && [...textTracks].map((track) => (
1522
<li
23+
key={track.language}
1624
className={track.mode === 'showing'
17-
? styles.activeTrack
25+
? styles.activeTrackItem
1826
: styles.trackItem}
19-
onClick={() => {
20-
const videoEl = getVideoEl()
21-
if (track.mode !== 'showing') {
22-
videoEl.textTracks[i].mode = 'showing';
23-
} else {
24-
videoEl.textTracks[i].mode = 'disabled';
25-
}
26-
// There is no `onTrackChange` event so we
27-
// have to forcibly update the video state
28-
// in order for the UI to update.
29-
forceUpdateState();
30-
}}
31-
key={track.language}>
27+
onClick={onItemClick.bind(this, track)}>
3228
{ track.label }
3329
</li>
3430
))}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
import { shallow } from 'enzyme';
3+
import Captions from './Captions';
4+
import styles from './Captions.css';
5+
6+
describe('Captions', () => {
7+
let component;
8+
9+
beforeAll(() => {
10+
component = shallow(<Captions />);
11+
});
12+
13+
it('should accept a className prop and append it to the components class', () => {
14+
const newClassNameString = 'a new className';
15+
expect(component.prop('className'))
16+
.toContain(styles.component);
17+
component.setProps({
18+
className: newClassNameString
19+
});
20+
expect(component.prop('className'))
21+
.toContain(styles.component);
22+
expect(component.prop('className'))
23+
.toContain(newClassNameString);
24+
});
25+
});

src/DefaultPlayer/DefaultPlayer.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, { PropTypes } from 'react';
22
import videoConnect from './../video/video';
33
import {
44
setVolume,
5+
showTrack,
6+
toggleTracks,
57
fullscreen,
68
toggleMute,
79
togglePause,
@@ -13,6 +15,7 @@ import styles from './DefaultPlayer.css';
1315
import Time from './Time/Time';
1416
import Seek from './Seek/Seek';
1517
import Volume from './Volume/Volume';
18+
import Captions from './Captions/Captions';
1619
import PlayPause from './PlayPause/PlayPause';
1720
import Fullscreen from './Fullscreen/Fullscreen';
1821
import ErrorMessage from './ErrorMessage/ErrorMessage';
@@ -26,8 +29,10 @@ export const DefaultPlayer = ({
2629
onSeekChange,
2730
onVolumeChange,
2831
onVolumeClick,
32+
onCaptionsClick,
2933
onPlayPauseClick,
3034
onFullscreenClick,
35+
onCaptionsItemClick,
3136
...restProps
3237
}) => {
3338
return (
@@ -76,6 +81,14 @@ export const DefaultPlayer = ({
7681
onChange={onVolumeChange}
7782
onClick={onVolumeClick}
7883
{...video} />;
84+
case 'Captions':
85+
return video && video.textTracks && video.textTracks.length
86+
? <Captions
87+
key={i}
88+
onClick={onCaptionsClick}
89+
onItemClick={onCaptionsItemClick}
90+
{...video}/>
91+
: null;
7992
default:
8093
return null;
8194
}
@@ -86,7 +99,7 @@ export const DefaultPlayer = ({
8699
);
87100
};
88101

89-
const controls = ['PlayPause', 'Seek', 'Time', 'Volume', 'Fullscreen'];
102+
const controls = ['PlayPause', 'Seek', 'Time', 'Volume', 'Fullscreen', 'Captions'];
90103

91104
DefaultPlayer.defaultProps = {
92105
controls
@@ -109,7 +122,9 @@ export default videoConnect(
109122
(videoEl, state) => ({
110123
onFullscreenClick: () => fullscreen(videoEl),
111124
onVolumeClick: () => toggleMute(videoEl, state),
125+
onCaptionsClick: () => toggleTracks(state),
112126
onPlayPauseClick: () => togglePause(videoEl, state),
127+
onCaptionsItemClick: (track) => showTrack(state, track),
113128
onVolumeChange: (e) => setVolume(videoEl, state, e.target.value),
114129
onSeekChange: (e) => setCurrentTime(videoEl, state, e.target.value * state.duration / 100)
115130
})

src/video/api.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,30 @@ export const fullscreen = (videoEl) => {
5050
} else if (videoEl.webkitRequestFullscreen) {
5151
videoEl.webkitRequestFullscreen();
5252
}
53-
}
53+
};
54+
55+
export const showTrack = ({ textTracks }, track) => {
56+
hideTracks({ textTracks });
57+
track.mode = 'showing';
58+
};
59+
60+
export const hideTracks = ({ textTracks }) => {
61+
for (var i = 0; i < textTracks.length; i++) {
62+
textTracks[i].mode = 'disabled';
63+
}
64+
};
65+
66+
export const toggleTracks = (() => {
67+
let previousTrack;
68+
return ({ textTracks }) => {
69+
let currentTrack = [...textTracks].filter((track) => track.mode === 'showing')[0];
70+
if (currentTrack) {
71+
hideTracks({ textTracks });
72+
previousTrack = currentTrack;
73+
} else {
74+
showTrack({ textTracks }, previousTrack || textTracks[0]);
75+
}
76+
}})();
5477

5578
/**
5679
* Custom getter methods that are commonly used

src/video/api.test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,35 @@ import {
22
mute,
33
unmute,
44
setVolume,
5+
showTrack,
56
toggleMute,
67
fullscreen,
8+
hideTracks,
79
togglePause,
10+
toggleTracks,
811
setCurrentTime,
912
getPercentagePlayed
1013
} from './api';
1114

1215
describe('api', () => {
1316
let videoElMock;
17+
let textTracksMock;
1418

1519
beforeEach(() => {
1620
videoElMock = {
1721
play: jest.fn(),
1822
pause: jest.fn()
1923
};
24+
textTracksMock = [{
25+
id: 1,
26+
mode: 'showing'
27+
}, {
28+
id: 2,
29+
mode: 'disabled'
30+
}, {
31+
id: 3,
32+
mode: 'disabled'
33+
}];
2034
});
2135

2236
describe('togglePause', () => {
@@ -131,6 +145,59 @@ describe('api', () => {
131145
});
132146
});
133147

148+
describe('hideTracks', () => {
149+
it('hides all of the tracks', () => {
150+
expect(textTracksMock[0].mode).toBe('showing');
151+
hideTracks({ textTracks: textTracksMock }, textTracksMock[2]);
152+
expect(textTracksMock[0].mode).toBe('disabled');
153+
});
154+
});
155+
156+
describe('showTrack', () => {
157+
it('hides all of the tracks', () => {
158+
expect(textTracksMock[0].mode).toBe('showing');
159+
showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
160+
expect(textTracksMock[0].mode).toBe('disabled');
161+
});
162+
163+
it('sets the given track to show', () => {
164+
expect(textTracksMock[2].mode).toBe('disabled');
165+
showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
166+
expect(textTracksMock[2].mode).toBe('showing');
167+
});
168+
});
169+
170+
describe('toggleTracks', () => {
171+
it('shows the first track if no tracks are showing and there is no previously active track', () => {
172+
textTracksMock[0].mode = 'disabled';
173+
expect(textTracksMock[0].mode).toBe('disabled');
174+
toggleTracks({ textTracks: textTracksMock });
175+
expect(textTracksMock[0].mode).toBe('showing');
176+
});
177+
178+
it('hides all tracks if a current track is showing', () => {
179+
expect(textTracksMock[0].mode).toBe('showing');
180+
toggleTracks({ textTracks: textTracksMock });
181+
expect(textTracksMock[0].mode).toBe('disabled');
182+
expect(textTracksMock[1].mode).toBe('disabled');
183+
expect(textTracksMock[2].mode).toBe('disabled');
184+
});
185+
186+
it('shows the previously active track if no tracks are showing', () => {
187+
expect(textTracksMock[0].mode).toBe('showing');
188+
toggleTracks({ textTracks: textTracksMock });
189+
expect(textTracksMock[0].mode).toBe('disabled');
190+
toggleTracks({ textTracks: textTracksMock });
191+
expect(textTracksMock[0].mode).toBe('showing');
192+
showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
193+
expect(textTracksMock[2].mode).toBe('showing');
194+
toggleTracks({ textTracks: textTracksMock });
195+
expect(textTracksMock[2].mode).toBe('disabled');
196+
toggleTracks({ textTracks: textTracksMock });
197+
expect(textTracksMock[2].mode).toBe('showing');
198+
});
199+
});
200+
134201
describe('getPercentagePlayed', () => {
135202
it('returns correct percentage played', () => {
136203
expect(getPercentagePlayed({

src/video/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export const EVENTS = [
2424
'onWaiting'
2525
];
2626

27+
export const TRACKEVENTS = [
28+
'onChange',
29+
'onAddTrack',
30+
'onRemoveTrack'
31+
];
32+
2733
export const METHODS = [
2834
'addTextTrack',
2935
'canPlayType',

src/video/video.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import toClass from 'recompose/toClass';
99
import {
1010
EVENTS,
1111
PROPERTIES,
12+
TRACKEVENTS
1213
} from './constants';
1314

1415
const defaultMapStateToProps = (state = {}) => Object.assign({
@@ -56,6 +57,10 @@ export default (
5657
this.videoEl[event.toLowerCase()] = this.updateState;
5758
});
5859

60+
TRACKEVENTS.forEach(event => {
61+
this.videoEl.textTracks[event.toLowerCase()] = this.updateState;
62+
});
63+
5964
// If <source> elements are used instead of a src attribute then
6065
// errors for unsupported format do not bubble up to the <video>.
6166
// Do this manually by listening to the last <source> error event

0 commit comments

Comments
 (0)