Skip to content

Commit beed139

Browse files
committed
Source not supported error handling
1 parent 658a73c commit beed139

File tree

7 files changed

+143
-50
lines changed

7 files changed

+143
-50
lines changed

demo/src/components/App.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import pkg from './../../../package.json';
66
import vttEn from './../../assets/sintel-en.vtt';
77
import vttEs from './../../assets/sintel-es.vtt';
88

9-
const sintelTrailer = 'http://media.w3.org/2010/05/sintel/trailer.mp4';
9+
const sintelTrailer = 'https://download.blender.org/durian/trailer/sintel_trailer-720p.mp4';
10+
const bigBuckBunny = 'http://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_h264.mov';
11+
const failingSource = 'https://github.com/mderrick/react-html5video';
1012

1113
const App = () => (
1214
<div className={styles.component}>
@@ -25,6 +27,21 @@ const App = () => (
2527
srcLang="es"
2628
src={vttEs} />
2729
</Video>
30+
<Video
31+
src={bigBuckBunny}
32+
className={styles.video}>
33+
</Video>
34+
<Video
35+
src={failingSource}
36+
className={styles.video}>
37+
</Video>
38+
<Video className={styles.video}>
39+
<source src={failingSource} type="video/mp4" />
40+
<source src={sintelTrailer} type="video/mp4" />
41+
</Video>
42+
<Video className={styles.video}>
43+
<source src={failingSource} type="video/mp4" />
44+
</Video>
2845
</div>
2946
);
3047

src/DefaultPlayer/DefaultPlayer.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
position: relative;
33
font-family: Helvetica;
44
font-size: 11px;
5+
background-color: #000;
56
}
67

78
.video {
89
width: 100%;
910
height: 100%;
1011
}
1112

13+
.error {
14+
position: absolute;
15+
top: 0;
16+
right: 0;
17+
bottom: 0;
18+
left: 0;
19+
}
20+
1221
.controls {
1322
position: absolute;
1423
bottom: 0;

src/DefaultPlayer/DefaultPlayer.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,30 @@ import styles from './DefaultPlayer.css';
1010
import PlayPause from './PlayPause/PlayPause';
1111
import Seek from './Seek/Seek';
1212
import Volume from './Volume/Volume';
13+
import ErrorMessage from './ErrorMessage/ErrorMessage';
1314

14-
const DefaultPlayer = (props) => {
15-
const {
16-
video,
17-
style,
18-
children,
19-
className,
20-
setVolume,
21-
toggleMute,
22-
togglePause,
23-
setCurrentTime,
24-
...restProps
25-
} = props;
15+
const DefaultPlayer = ({
16+
video,
17+
style,
18+
children,
19+
className,
20+
setVolume,
21+
toggleMute,
22+
togglePause,
23+
setCurrentTime,
24+
...restProps
25+
}) => {
2626
return (
2727
<div className={[
2828
styles.component,
2929
className
3030
].join(' ')}
3131
style={style}>
32+
{ video.error
33+
? <ErrorMessage
34+
className={styles.error}
35+
{...video} />
36+
: null }
3237
<video
3338
className={styles.video}
3439
{...restProps}>
@@ -50,9 +55,18 @@ const DefaultPlayer = (props) => {
5055
);
5156
};
5257

53-
export default video(DefaultPlayer, undefined, (videoEl, state) => ({
54-
toggleMute: () => toggleMute(videoEl, state),
55-
togglePause: () => togglePause(videoEl, state),
56-
setVolume: (value) => setVolume(videoEl, state, value),
57-
setCurrentTime: (value) => setCurrentTime(videoEl, state, value)
58-
}));
58+
export default video(
59+
DefaultPlayer,
60+
({ networkState, error, ...restState }) => ({
61+
video: {
62+
error: error || networkState === 3,
63+
...restState
64+
}
65+
}),
66+
(videoEl, state) => ({
67+
toggleMute: () => toggleMute(videoEl, state),
68+
togglePause: () => togglePause(videoEl, state),
69+
setVolume: (value) => setVolume(videoEl, state, value),
70+
setCurrentTime: (value) => setCurrentTime(videoEl, state, value)
71+
})
72+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.component {
2+
color: #fff;
3+
text-align: center;
4+
}
5+
6+
.inner {
7+
position: absolute;
8+
top: 50%;
9+
right: 0;
10+
left: 0;
11+
transform: translateY(-50%);
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import styles from './ErrorMessage.css';
3+
4+
export default ({ className }) => (
5+
<div className={[
6+
styles.component,
7+
className
8+
].join(' ')}>
9+
<span className={styles.inner}>
10+
There has been an error.
11+
</span>
12+
</div>
13+
);

src/video/video.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ export default (
5555
EVENTS.forEach(event => {
5656
this.videoEl[event.toLowerCase()] = this.updateState;
5757
});
58+
59+
// If <source> elements are used instead of a src attribute then
60+
// errors for unsuppored format do not bubble up to the <video>.
61+
// Do this manually by listening to the last <source> error event
62+
// to force an update.
63+
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_HTML5_audio_and_video
64+
const sources = this.videoEl.getElementsByTagName('source');
65+
if (sources.length) {
66+
const lastSource = sources[sources.length - 1];
67+
lastSource.addEventListener('error', this.updateState);
68+
}
5869
}
5970

6071
// Stop `this.el` from being null briefly on every render,

src/video/video.test.js

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ const TestVideo = ({ video, ...restProps }) => {
1616
delete restProps.videoEl;
1717
return (
1818
<div>
19-
<video {...restProps}></video>
19+
<video {...restProps}>
20+
<source src="1" />
21+
</video>
2022
<TestControl {...video} />
2123
</div>
2224
);
@@ -31,41 +33,56 @@ describe('video', () => {
3133
});
3234

3335
describe('the wrapped component', () => {
34-
beforeAll(() => {
36+
beforeEach(() => {
3537
component = mount(
3638
<Component autoPlay />
3739
);
3840
});
3941

40-
it('should receive all of the HTMLMediaElement API as props when an event is triggered', () => {
41-
const testControl = component.find(TestControl);
42-
expect(testControl.props()).toEqual({});
43-
component.find('video').node.dispatchEvent(new Event('play'));
44-
45-
// Only matching a subset is sufficient.
46-
expect(testControl.props()).toMatchObject({
47-
controller: undefined,
48-
autoPlay: undefined,
49-
controls: false,
50-
currentSrc: '',
51-
currentTime: 0,
52-
defaultMuted: false,
53-
defaultPlaybackRate: 1,
54-
duration: 0,
55-
ended: false,
56-
error: undefined,
57-
loop: false,
58-
mediaGroup: undefined,
59-
muted: false,
60-
networkState: 0,
61-
paused: true,
62-
playbackRate: 1,
63-
preload: '',
64-
readyState: 0,
65-
seeking: false,
66-
src: '',
67-
startDate: undefined,
68-
volume: 1
42+
describe('HTMLMediaElement API as props', () => {
43+
let testControl;
44+
beforeEach(() => {
45+
component = mount(
46+
<Component autoPlay />
47+
);
48+
testControl = component.find(TestControl);
49+
expect(testControl.props()).toEqual({});
50+
});
51+
52+
it('should be provided when a video event is triggered', () => {
53+
component.find('video').node.dispatchEvent(new Event('play'));
54+
});
55+
56+
it('should be provided when an error occurs on last source element', () => {
57+
component.find('source').node.dispatchEvent(new Event('error'));
58+
});
59+
60+
afterEach(() => {
61+
// Only matching a subset is sufficient.
62+
expect(testControl.props()).toMatchObject({
63+
controller: undefined,
64+
autoPlay: undefined,
65+
controls: false,
66+
currentSrc: '',
67+
currentTime: 0,
68+
defaultMuted: false,
69+
defaultPlaybackRate: 1,
70+
duration: 0,
71+
ended: false,
72+
error: undefined,
73+
loop: false,
74+
mediaGroup: undefined,
75+
muted: false,
76+
networkState: 0,
77+
paused: true,
78+
playbackRate: 1,
79+
preload: '',
80+
readyState: 0,
81+
seeking: false,
82+
src: '',
83+
startDate: undefined,
84+
volume: 1
85+
});
6986
});
7087
});
7188
});

0 commit comments

Comments
 (0)