├── .nvmrc
├── .eslintignore
├── browserslist
├── __mocks__
├── fileMock.js
└── fileTransformer.js
├── .gitignore
├── demo
├── .babelrc
├── assets
│ ├── static
│ │ ├── example.png
│ │ └── example.v2.png
│ ├── poster-big-buck-bunny.png
│ ├── poster-sintel-trailer.png
│ ├── sintel-en.vtt
│ └── sintel-es.vtt
├── package.json
├── webpack.config.js
├── src
│ ├── components
│ │ ├── App.css
│ │ ├── App.test.js
│ │ └── App.js
│ ├── entry.js
│ └── index.html
└── generateConfig.js
├── postcss.config.js
├── webpack.config.js
├── .babelrc
├── src
├── DefaultPlayer
│ ├── Time
│ │ ├── Time.css
│ │ ├── Time.js
│ │ └── Time.test.js
│ ├── copy.js
│ ├── Icon
│ │ ├── play_arrow.svg
│ │ ├── volume_mute.svg
│ │ ├── pause.svg
│ │ ├── volume_down.svg
│ │ ├── fullscreen.svg
│ │ ├── fullscreen_exit.svg
│ │ ├── report.svg
│ │ ├── volume_up.svg
│ │ ├── spin.svg
│ │ ├── volume_off.svg
│ │ └── closed_caption.svg
│ ├── Fullscreen
│ │ ├── Fullscreen.css
│ │ ├── Fullscreen.js
│ │ └── Fullscreen.test.js
│ ├── PlayPause
│ │ ├── PlayPause.css
│ │ ├── PlayPause.js
│ │ └── PlayPause.test.js
│ ├── DefaultPlayer.css
│ ├── Seek
│ │ ├── Seek.css
│ │ ├── Seek.js
│ │ └── Seek.test.js
│ ├── Overlay
│ │ ├── Overlay.css
│ │ ├── Overlay.js
│ │ └── Overlay.test.js
│ ├── Captions
│ │ ├── Captions.css
│ │ ├── Captions.test.js
│ │ └── Captions.js
│ ├── Volume
│ │ ├── Volume.css
│ │ ├── Volume.js
│ │ └── Volume.test.js
│ ├── DefaultPlayer.test.js
│ └── DefaultPlayer.js
├── entry.js
└── video
│ ├── constants.js
│ ├── api.js
│ ├── video.js
│ ├── video.test.js
│ └── api.test.js
├── .editorconfig
├── .eslintrc.json
├── LICENSE.md
├── scripts
├── deploy:demo.js
└── start.js
├── .travis.yml
├── generateConfig.js
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 6
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | > 1%
2 | Last 5 versions
3 | not ie < 9
4 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 | dist
5 |
--------------------------------------------------------------------------------
/demo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./../.babelrc",
3 | "plugins": ["react-hot-loader/babel"]
4 | }
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ]
5 | };
6 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const generateConfig = require('./generateConfig');
2 |
3 | module.exports = generateConfig();
4 |
--------------------------------------------------------------------------------
/demo/assets/static/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mderrick/react-html5video/HEAD/demo/assets/static/example.png
--------------------------------------------------------------------------------
/demo/assets/static/example.v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mderrick/react-html5video/HEAD/demo/assets/static/example.v2.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": ["transform-runtime", "transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/demo/assets/poster-big-buck-bunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mderrick/react-html5video/HEAD/demo/assets/poster-big-buck-bunny.png
--------------------------------------------------------------------------------
/demo/assets/poster-sintel-trailer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mderrick/react-html5video/HEAD/demo/assets/poster-sintel-trailer.png
--------------------------------------------------------------------------------
/__mocks__/fileTransformer.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | process(src, filename) {
5 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-html5video-demo",
3 | "dependencies": {
4 | "react": "^16.0.0-rc.3",
5 | "react-dom": "^16.0.0-rc.3",
6 | "react-html5video": "file:../",
7 | "reset-css": "^2.2.0"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Time/Time.css:
--------------------------------------------------------------------------------
1 | .component {
2 | padding: 0 10px 0 10px;
3 | line-height: 35px;
4 | color: #fff;
5 | }
6 |
7 | .current {
8 | margin-right: 5px;
9 | }
10 |
11 | .duration {
12 | margin-left: 5px;
13 | color: #919191;
14 | }
15 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | var generateConfig = require('./generateConfig');
2 | var pkg = require('./../package.json');
3 |
4 | module.exports = generateConfig({
5 | hot: false,
6 | optimize: true,
7 | extractCss: true,
8 | publicPath: '/' + pkg.name + '/'
9 | });
10 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/copy.js:
--------------------------------------------------------------------------------
1 | const copy = {
2 | play: 'Play video',
3 | pause: 'Pause video',
4 | mute: 'Mute video',
5 | unmute: 'Unmute video',
6 | volume: 'Change volume',
7 | fullscreen: 'View video fullscreen',
8 | seek: 'Seek video',
9 | captions: 'Toggle captions'
10 | };
11 |
12 | export default copy;
13 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/play_arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/volume_mute.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/demo/assets/sintel-en.vtt:
--------------------------------------------------------------------------------
1 | WEBVTT - Sintel Caption File
2 |
3 | Sage
4 | 00:00:12.000 --> 00:00:15.000
5 | What brings you to the land
6 | of the gatekeepers?
7 |
8 | Searching
9 | 00:00:18.500 --> 00:00:20.500
10 | I'm searching for someone.
11 |
12 | Quest
13 | 00:00:36.500 --> 00:00:39.000
14 | A dangerous quest for a lone hunter.
15 |
16 | Alone
17 | 00:00:41.500 --> 00:00:44.000
18 | I've been alone for as long
19 | as I can remember.
20 |
--------------------------------------------------------------------------------
/demo/assets/sintel-es.vtt:
--------------------------------------------------------------------------------
1 | WEBVTT - Spanish Sintel Caption File
2 |
3 | Sage
4 | 00:00:12.000 --> 00:00:15.000
5 | Que te trae a la tierra
6 | de los porteros?
7 |
8 | Searching
9 | 00:00:18.500 --> 00:00:20.500
10 | Estoy buscando a alguien.
11 |
12 | Quest
13 | 00:00:36.500 --> 00:00:39.000
14 | Una busqueda peligrosa para un cazador solitario.
15 |
16 | Alone
17 | 00:00:41.500 --> 00:00:44.000
18 | He estado sola desde que recuerdo.
19 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/pause.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Fullscreen/Fullscreen.css:
--------------------------------------------------------------------------------
1 | .component {}
2 |
3 | .component:hover {
4 | background-color: #000;
5 | }
6 |
7 | .button {
8 | width: 34px;
9 | height: 34px;
10 | background: none;
11 | border: 0;
12 | color: inherit;
13 | font: inherit;
14 | line-height: normal;
15 | overflow: visible;
16 | padding: 0;
17 | cursor: pointer;
18 | }
19 |
20 | .button:focus {
21 | outline: 0;
22 | }
23 |
24 | .icon {
25 | padding: 5px;
26 | }
27 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/PlayPause/PlayPause.css:
--------------------------------------------------------------------------------
1 | .component {}
2 |
3 | .component:hover {
4 | background-color: #000;
5 | }
6 |
7 | .button {
8 | width: 34px;
9 | height: 34px;
10 | background: none;
11 | border: 0;
12 | color: inherit;
13 | font: inherit;
14 | line-height: normal;
15 | overflow: visible;
16 | padding: 0;
17 | cursor: pointer;
18 | }
19 |
20 | .button:focus {
21 | outline: 0;
22 | }
23 |
24 | .icon {
25 | padding: 5px;
26 | }
27 |
--------------------------------------------------------------------------------
/src/entry.js:
--------------------------------------------------------------------------------
1 | import videoConnect from './video/video';
2 | import * as apiHelpers from './video/api';
3 | import DefaultPlayer, {
4 | Time,
5 | Seek,
6 | Volume,
7 | Captions,
8 | PlayPause,
9 | Fullscreen,
10 | Overlay
11 | } from './DefaultPlayer/DefaultPlayer';
12 |
13 | export {
14 | videoConnect as default,
15 | apiHelpers,
16 | DefaultPlayer,
17 | Time,
18 | Seek,
19 | Volume,
20 | Captions,
21 | PlayPause,
22 | Fullscreen,
23 | Overlay
24 | };
25 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig: http://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 4
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
16 | [{package.json,.travis.yml}]
17 | # The indent size used in the `package.json` file cannot be changed
18 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516
19 | indent_size = 2
20 | indent_style = space
21 |
--------------------------------------------------------------------------------
/demo/src/components/App.css:
--------------------------------------------------------------------------------
1 | .component {
2 | padding: 20px;
3 | font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
4 | }
5 |
6 | .header {
7 | margin-bottom: 20px;
8 | }
9 |
10 | .title {
11 | font-size: 30px;
12 | margin-bottom: 5px;
13 | }
14 |
15 | .link {
16 | color: #2492a8;
17 | }
18 |
19 | .videoListItem {
20 | background-color: #efefef;
21 | padding: 20px;
22 | margin-bottom: 20px;
23 | max-width: 634px;
24 | max-height: 356px;
25 | margin: 0 auto;
26 | }
27 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/volume_down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/demo/src/entry.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | import React from 'react';
4 | import { render } from 'react-dom';
5 | import App from './components/App';
6 |
7 | render(
8 | ,
9 | document.getElementById('content')
10 | );
11 |
12 | if (module.hot) {
13 | module.hot.accept('./components/App', () => {
14 | const NextRoot = require('./components/App').default;
15 | render(
16 | ,
17 | document.getElementById('content')
18 | );
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/fullscreen_exit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/report.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/DefaultPlayer.css:
--------------------------------------------------------------------------------
1 | .component {
2 | position: relative;
3 | font-family: Helvetica;
4 | font-size: 11px;
5 | background-color: #000;
6 | }
7 |
8 | .video {
9 | width: 100%;
10 | height: 100%;
11 | }
12 |
13 | .controls {
14 | position: absolute;
15 | bottom: 0;
16 | right: 0;
17 | left: 0;
18 | height: 34px;
19 | display: flex;
20 | background-color: rgba(0,0,0,0.7);
21 | opacity: 0;
22 | transition: opacity 0.2s;
23 | }
24 |
25 | .seek {
26 | flex-grow: 1;
27 | }
28 |
29 | .component:hover .controls {
30 | opacity: 1;
31 | }
32 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Seek/Seek.css:
--------------------------------------------------------------------------------
1 | .component {
2 | position: relative;
3 | }
4 |
5 | .track {
6 | position: absolute;
7 | top: 50%;
8 | left: 5px;
9 | right: 5px;
10 | height: 4px;
11 | transform: translateY(-50%);
12 | background-color: #3e3e3e;
13 | }
14 |
15 | .buffer,
16 | .fill,
17 | .input {
18 | position: absolute;
19 | top: 0;
20 | left: 0;
21 | height: 100%;
22 | }
23 |
24 | .buffer {
25 | background-color: #5a5a5a;
26 | }
27 |
28 | .fill {
29 | background: #fff;
30 | }
31 |
32 | .input {
33 | width: 100%;
34 | opacity: 0;
35 | cursor: pointer;
36 | }
37 |
--------------------------------------------------------------------------------
/demo/src/components/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import App from './App';
5 | import styles from './App.css';
6 |
7 | describe('App', () => {
8 | let component;
9 |
10 | beforeEach(() => {
11 | component = shallow(
12 |
13 | );
14 | });
15 |
16 | it('contains heading text', () => {
17 | expect(component.find('h1').text())
18 | .toEqual('React HTML5 Video');
19 | });
20 |
21 | it('has a className', () => {
22 | expect(component.prop('className'))
23 | .toEqual(styles.component);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/volume_up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/spin.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "commonjs": true,
6 | "es6": true,
7 | "jest": true,
8 | "jasmine": true
9 | },
10 | "extends": "eslint:recommended",
11 | "parserOptions": {
12 | "ecmaFeatures": {
13 | "experimentalObjectRestSpread": true,
14 | "jsx": true
15 | },
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "react"
20 | ],
21 | "rules": {
22 | "quotes": [
23 | "error",
24 | "single"
25 | ],
26 | "react/jsx-uses-react": "error",
27 | "react/jsx-uses-vars": "error"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React HTML5 Video Component
6 |
7 |
8 |
9 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Fullscreen/Fullscreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Fullscreen.css';
3 | import FullscreenIcon from './../Icon/fullscreen.svg';
4 |
5 | export default ({ onClick, className, ariaLabel }) => {
6 | return (
7 |
11 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Overlay/Overlay.css:
--------------------------------------------------------------------------------
1 | .component {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | height: 100%;
8 | width: 100%;
9 | color: #fff;
10 | text-align: center;
11 | cursor: pointer;
12 | background-color: rgba(0,0,0,0);
13 | }
14 |
15 | .inner {
16 | display: inline-block;
17 | position: absolute;
18 | top: 50%;
19 | right: 0;
20 | left: 50%;
21 | width: 60px;
22 | height: 60px;
23 | transform: translateY(-50%);
24 | margin-left: -30px;
25 | background-color: rgba(0,0,0,0.7);
26 | border-radius: 10px;
27 | }
28 |
29 | .icon {
30 | position: absolute;
31 | top: 50%;
32 | right: 0;
33 | left: 50%;
34 | margin-left: -20px;
35 | transform: translateY(-50%);
36 | }
37 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/volume_off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Icon/closed_caption.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Time/Time.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Time.css';
3 |
4 | const formatTime = (seconds) => {
5 | const date = new Date(Date.UTC(1970,1,1,0,0,0,0));
6 | seconds = isNaN(seconds) || seconds > 86400
7 | ? 0
8 | : Math.floor(seconds);
9 | date.setSeconds(seconds);
10 | const duration = date.toISOString().substr(11, 8).replace(/^0{1,2}:?0{0,1}/,'');
11 | return duration;
12 | };
13 |
14 | export default ({ currentTime, duration, className }) => {
15 | return (
16 |
20 |
21 | { formatTime(currentTime) }
22 |
23 | /
24 |
25 | { formatTime(duration) }
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/PlayPause/PlayPause.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './PlayPause.css';
3 | import PlayArrow from './../Icon/play_arrow.svg';
4 | import Pause from './../Icon/pause.svg';
5 |
6 | export default ({ onClick, paused, className, ariaLabelPlay, ariaLabelPause }) => {
7 | return (
8 |
12 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Captions/Captions.css:
--------------------------------------------------------------------------------
1 | .component {
2 | position: relative;
3 | }
4 |
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 | }
29 |
30 | .trackList {
31 | position: absolute;
32 | right: 0;
33 | bottom: 100%;
34 | display: none;
35 | background-color: rgba(0,0,0,0.7);
36 | list-style: none;
37 | padding: 0;
38 | margin: 0;
39 | color: #fff;
40 | }
41 |
42 | .component:hover .trackList {
43 | display: block;
44 | }
45 |
46 | .trackItem {
47 | padding: 7px;
48 | cursor: pointer;
49 | }
50 |
51 | .activeTrackItem,
52 | .trackItem:hover {
53 | background: #000;
54 | }
55 |
56 | .activeTrackItem {
57 | composes: trackItem;
58 | text-decoration: underline;
59 | }
60 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Captions/Captions.test.js:
--------------------------------------------------------------------------------
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(
11 |
12 | );
13 | });
14 |
15 | it('should accept a className prop and append it to the components class', () => {
16 | const newClassNameString = 'a new className';
17 | expect(component.prop('className'))
18 | .toContain(styles.component);
19 | component.setProps({
20 | className: newClassNameString
21 | });
22 | expect(component.prop('className'))
23 | .toContain(styles.component);
24 | expect(component.prop('className'))
25 | .toContain(newClassNameString);
26 | });
27 |
28 | it('has correct aria-label', () => {
29 | expect(component.find('button').prop('aria-label'))
30 | .toEqual('Captions');
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Matt Derrick
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/src/DefaultPlayer/Seek/Seek.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Seek.css';
3 |
4 | export default ({ onChange, percentagePlayed, percentageBuffered, className, ariaLabel }) => {
5 | return (
6 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Captions/Captions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Captions.css';
3 | import ClosedCaptionIcon from './../Icon/closed_caption.svg';
4 |
5 | export default ({ textTracks, onClick, onItemClick, className, ariaLabel }) => {
6 | return (
7 |
11 |
20 |
21 | { textTracks && [...textTracks].map((track) => (
22 | -
28 | { track.label }
29 |
30 | ))}
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Volume/Volume.css:
--------------------------------------------------------------------------------
1 | .component {
2 | position: relative;
3 | }
4 |
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: 7px;
28 | }
29 |
30 | .slider {
31 | display: none;
32 | position: absolute;
33 | right: 5px;
34 | bottom: 100%;
35 | left: 5px;
36 | height: 56px;
37 | background-color: #000;
38 | }
39 |
40 | .component:hover .slider {
41 | display: block;
42 | }
43 |
44 | .track {
45 | position: absolute;
46 | top: 8px;
47 | bottom: 8px;
48 | left: 50%;
49 | width: 4px;
50 | transform: translateX(-50%);
51 | background-color: #3e3e3e;
52 | }
53 |
54 | .fill,
55 | .input {
56 | position: absolute;
57 | right: 0;
58 | bottom: 0;
59 | left: 0;
60 | height: 100%;
61 | width: 100%;
62 | }
63 |
64 | .fill {
65 | background-color: #fff;
66 | }
67 |
68 | .input {
69 | padding: 0;
70 | margin: 0;
71 | opacity: 0;
72 | -webkit-appearance: slider-vertical;
73 | cursor: pointer;
74 | }
--------------------------------------------------------------------------------
/src/video/constants.js:
--------------------------------------------------------------------------------
1 | export const EVENTS = [
2 | 'abort',
3 | 'canPlay',
4 | 'canPlayThrough',
5 | 'durationChange',
6 | 'emptied',
7 | 'encrypted',
8 | 'ended',
9 | 'error',
10 | 'loadedData',
11 | 'loadedMetadata',
12 | 'loadStart',
13 | 'pause',
14 | 'play',
15 | 'playing',
16 | 'progress',
17 | 'rateChange',
18 | 'seeked',
19 | 'seeking',
20 | 'stalled',
21 | 'suspend',
22 | 'timeUpdate',
23 | 'volumeChange',
24 | 'waiting'
25 | ];
26 |
27 | export const TRACKEVENTS = [
28 | 'change',
29 | 'addTrack',
30 | 'removeTrack'
31 | ];
32 |
33 | export const METHODS = [
34 | 'addTextTrack',
35 | 'canPlayType',
36 | 'load',
37 | 'play',
38 | 'pause'
39 | ];
40 |
41 | export const PROPERTIES = [
42 | 'audioTracks',
43 | 'autoPlay',
44 | 'buffered',
45 | 'controller',
46 | 'controls',
47 | 'currentSrc',
48 | 'currentTime',
49 | 'defaultMuted',
50 | 'defaultPlaybackRate',
51 | 'duration',
52 | 'ended',
53 | 'error',
54 | 'loop',
55 | 'mediaGroup',
56 | 'muted',
57 | 'networkState',
58 | 'paused',
59 | 'playbackRate',
60 | 'played',
61 | 'preload',
62 | 'readyState',
63 | 'seekable',
64 | 'seeking',
65 | 'src',
66 | 'startDate',
67 | 'textTracks',
68 | 'videoTracks',
69 | 'volume'
70 | ];
71 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Fullscreen/Fullscreen.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Fullscreen from './Fullscreen';
4 | import styles from './Fullscreen.css';
5 |
6 | describe('Fullscreen', () => {
7 | let component;
8 |
9 | beforeAll(() => {
10 | component = shallow(
11 |
12 | );
13 | });
14 |
15 | it('should accept a className prop and append it to the components class', () => {
16 | const newClassNameString = 'a new className';
17 | expect(component.prop('className'))
18 | .toContain(styles.component);
19 | component.setProps({
20 | className: newClassNameString
21 | });
22 | expect(component.prop('className'))
23 | .toContain(styles.component);
24 | expect(component.prop('className'))
25 | .toContain(newClassNameString);
26 | });
27 |
28 | it('triggers \'onClick\' prop when the button is clicked', () => {
29 | const spy = jest.fn();
30 | component.setProps({
31 | onClick: spy
32 | });
33 | expect(spy)
34 | .not.toHaveBeenCalled();
35 | component.find('button').simulate('click');
36 | expect(spy)
37 | .toHaveBeenCalled();
38 | });
39 |
40 | it('has correct aria-label', () => {
41 | expect(component.find('button').prop('aria-label'))
42 | .toEqual('Fullscreen');
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/scripts/deploy:demo.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const ghpages = require('gh-pages');
4 | const pkg = require('./../package.json');
5 | const path = require('path');
6 | const chalk = require('chalk');
7 | const fs = require('fs');
8 | const distPath = path.join(__dirname, '../demo/dist');
9 |
10 | const deploy = (options = {}) => {
11 | ghpages.publish(distPath, Object.assign({
12 | message: pkg.version
13 | }, options), (err) => {
14 | if (err) {
15 | error([err]);
16 | return;
17 | }
18 | console.log(chalk.green('Demo has succesfully deployed.'));
19 | });
20 | };
21 |
22 | const error = (errs = []) => {
23 | errs.forEach((err) => {
24 | console.log(chalk.red(err));
25 | });
26 | process.exit(1);
27 | };
28 |
29 | try {
30 | fs.accessSync(distPath, fs.F_OK);
31 | if (process.env.TRAVIS) {
32 | if (process.env.GITHUB_TOKEN) {
33 | deploy({
34 | repo: `https://${process.env.GITHUB_TOKEN}@github.com/${process.env.TRAVIS_REPO_SLUG}.git`,
35 | user: {
36 | name: 'Travis CI'
37 | }
38 | });
39 | } else {
40 | error(['process.env.GITHUB_TOKEN with "repo" access is required to deploy gh-pages.']);
41 | }
42 | } else {
43 | // Deploys using git origin, username and email.
44 | deploy();
45 | }
46 | } catch (e) {
47 | error([
48 | `${distPath} does not exist.`,
49 | 'Please run "npm i && npm run i:demo && npm run build:demo" and try again.'
50 | ]);
51 | }
52 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Overlay/Overlay.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styles from './Overlay.css';
3 | import PlayArrow from './../Icon/play_arrow.svg';
4 | import Spin from './../Icon/spin.svg';
5 | import Report from './../Icon/report.svg';
6 |
7 | export default class Overlay extends Component {
8 | renderContent () {
9 | const {
10 | error,
11 | paused,
12 | loading
13 | } = this.props;
14 | const iconProps = {
15 | className: styles.icon,
16 | height: 40,
17 | width: 40,
18 | fill: '#fff'
19 | };
20 | if (error) {
21 | return (
22 |
23 |
24 |
25 | );
26 | } else if (loading) {
27 | return (
28 |
29 |
30 |
31 | );
32 | } else if (paused) {
33 | return (
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | render () {
42 | const { className, onClick } = this.props;
43 | return (
44 |
49 | { this.renderContent() }
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | script:
3 | - npm run install:demo
4 | - npm run build:demo
5 | - npm run lint
6 | - npm test
7 | deploy:
8 | provider: npm
9 | email: me@mattderrick.co.uk
10 | api_key:
11 | secure: ge4pjksEwP8kKRdS3nq1Bq7gPH9t9/G44vMqUd5BO5puR760jucvXCdqJ9isBzAHyXE7+0jbXmpE1KbUsiGqkasGEYsxtJYvx1Lb2F37e6W7QDRWxmN1AG0+MkTl49+RQFaL9uMLSWl9yStRWnzljcqbK8Jcnt8cE+JU1zvwCkKnokysyCbL7pz8w22fwuXZP8wg8wYZQL5c9mYRtNyUKFloNfADMEO1Zl0ZbpgBJdiT/MXtY6F1Y7mdCc+fCPucv0kbxoXm/kNsryYB0iLdtqwn3TW56Bwt+jaTEy2Wr62il1wST9/da4Iwzt5THLPO4+8sR2te45HkmaZmeRI92W6t7GUgseImTzYeFdxoyxXyziFH1hNTlOQuGDSie5DX87OXlEouILUmiLTcNZL0GrF6BALOyFB8ac/wtAO0OlVJqnMwlTI4s8K4yVBlP5cpRdPKVwbuwS/KX8pOhAmtRhyKgIBonsLuTp6GRkS1704Ui0nIJPjo65gOJIfnorYkKeJqrT6oDG1IUipCXX9vN5R0h00JqR3fOXsA1h6j1QFpg3hlG6UBXFWJ3SQdUUGjczy+z80sipRA2Kdf2TnFSKu1tVpRghWJ9bfmQmyN8NyzYCH9rxJj4K/Nh0TAi3J5Ns/6ypHwOg/9/s/8MhJUwE+FLaKy7NYl+RQXLnEeanA=
12 | skip_cleanup: true
13 | on:
14 | tags: true
15 | repo: mderrick/react-html5video
16 | after_deploy:
17 | - npm run deploy:demo
18 | env:
19 | global:
20 | secure: Jay4xSxm/nSDbbv7AghcCRCxbkzrX92SToFjW1ZvbnH/4gjgEHqNgaoZH/3mJ/s+1GyC9SCiAcOwlnCAGFabYdQNhwHBhQJFMK8v0WP2IKC2Vwh294sGPrCTvexRceumlh6y1la1KYYTuu6DDDsAaZ8eQcrK6hOcfuwl5xXB4xGNfshLavITs8a71F1RdX0cCa/ifSzbDjkfY1+1/o7vCCK595WP+WvTD/gA/OlpDygmtzJwg0400o9vmAlz7KzCLkk7pVakNODv3QvGHpd5n8jpzW30sHyKbnZfiqZPuw/6yV6NIYBmAXrviLUYQt07KU0IFWDO6cjc2H6puk3tL0yyZgZ0fMnin9mEGM0Rrf71zrtBH3XBhsLJGFK/vCuC1Kl+/mDSBrxm7koBawh7tzxOw7H2L3PMDin0A4MvUQS54ffGXd2AhNQKcf3w7eEb/kwdIOWPMcvDO7cca9OQxi50HB7bPOTsOIVflmCfgomBV2pwWAaAqcAGJni8dvp1BI8wN95bDLU1lSeTT/9RQlDRjykG5FzH/yBKhqVax+mtl6AxEFDHsZKE2Tc/mVqzpRChGJ9GF4N+E003qTaS6kTmqzYja/p9UL8aWThkQf4MnD91SR8duvD2wZOXoyNn+rSrrtZJ0p055mhms58ZbOSDgNPK8xSS0RZdPlwd+zo=
21 |
--------------------------------------------------------------------------------
/scripts/start.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const path = require('path');
4 | const pkg = require('./../package.json');
5 | const express = require('express');
6 | const webpack = require('webpack');
7 | const detect = require('detect-port');
8 | const chalk = require('chalk');
9 | const open = require('open');
10 | const webpackDevMiddleware = require('webpack-dev-middleware');
11 | const webpackHotMiddleware = require('webpack-hot-middleware');
12 | const generateDemoConfig = require('./../demo/generateConfig');
13 | const generateComponentConfig = require('./../generateConfig');
14 |
15 | const app = express();
16 | const compiler = webpack([
17 | generateComponentConfig({
18 | // Build the component library into node_modules
19 | // so we need not do a symlink for development.
20 | outputPath: path.resolve(__dirname, '../demo/node_modules/' + pkg.name + '/dist')
21 | }),
22 | generateDemoConfig({
23 | hot: true,
24 | optimize: false,
25 | extractCss: false
26 | })
27 | ]);
28 | const [componentCompiler, demoCompiler] = compiler.compilers;
29 |
30 | componentCompiler.watch({}, function(err) {
31 | if (err) {
32 | throw err;
33 | }
34 | });
35 | app.use(webpackDevMiddleware(demoCompiler));
36 | app.use(webpackHotMiddleware(demoCompiler));
37 |
38 | const run = (port) => {
39 | detect(port, (err, _port) => {
40 | if (port === _port) {
41 | app.listen(_port, () => {
42 | const url = `http://localhost:${port}`;
43 | console.log(chalk.cyan(`Server running at ${url}.`));
44 | open(url);
45 | });
46 | } else {
47 | run(port + 1);
48 | }
49 | });
50 | };
51 |
52 | run(6060);
53 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Overlay/Overlay.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Overlay from './Overlay';
4 | import styles from './Overlay.css';
5 | import PlayArrow from './../Icon/play_arrow.svg';
6 | import Spin from './../Icon/spin.svg';
7 | import Report from './../Icon/report.svg';
8 |
9 | describe('Overlay', () => {
10 | let component;
11 |
12 | beforeAll(() => {
13 | component = shallow();
14 | });
15 |
16 | it('should accept a className prop and append it to the components class', () => {
17 | const newClassNameString = 'a new className';
18 | expect(component.prop('className'))
19 | .toContain(styles.component);
20 | component.setProps({
21 | className: newClassNameString
22 | });
23 | expect(component.prop('className'))
24 | .toContain(styles.component);
25 | expect(component.prop('className'))
26 | .toContain(newClassNameString);
27 | });
28 |
29 | it('shows a PlayArrow icon if paused', () => {
30 | component.setProps({
31 | paused: true
32 | });
33 | expect(component.html())
34 | .toContain(PlayArrow);
35 | });
36 |
37 | it('shows Report icon if error regardless of loading or paused state', () => {
38 | component.setProps({
39 | error: true,
40 | loading: true,
41 | paused: true
42 | });
43 | expect(component.html())
44 | .toContain(Report);
45 | });
46 |
47 | it('shows Spin icon if loading regardless of paused state', () => {
48 | component.setProps({
49 | loading: true,
50 | paused: true,
51 | error: false
52 | });
53 | expect(component.html())
54 | .toContain(Spin);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/generateConfig.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
4 |
5 | const pkg = require('./package.json');
6 | const srcPath = path.resolve(__dirname, 'src');
7 | const distPath = path.resolve(__dirname, 'dist');
8 |
9 | module.exports = (options = {}) => {
10 | return {
11 | entry: [
12 | path.resolve(srcPath, 'entry.js')
13 | ],
14 | target: 'web',
15 | output: {
16 | path: options.outputPath || distPath,
17 | filename: 'index.js',
18 | libraryTarget: 'commonjs2',
19 | library: pkg.name
20 | },
21 | resolve: {
22 | extensions: ['.js', '.json', '.jsx', '']
23 | },
24 | externals: [{
25 | react: 'react',
26 | 'react-dom': 'react-dom',
27 | 'prop-types': 'prop-types'
28 | }],
29 | module: {
30 | loaders: [{
31 | test: /\.(js|jsx)$/,
32 | include: srcPath,
33 | loader: 'babel',
34 | query: {
35 | cacheDirectory: true
36 | }
37 | }, {
38 | test: /\.svg$/,
39 | loader: 'babel!react-svg'
40 | }, {
41 | test: /\.css$/,
42 | include: srcPath,
43 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=rh5v-[name]_[local]&-autoprefixer!postcss')
44 | }, {
45 | test: /\.(eot|ttf|woff|woff2)(\?.*)?$/,
46 | loader: 'url'
47 | }]
48 | },
49 | plugins: [
50 | new ExtractTextPlugin('styles.css'),
51 | new CaseSensitivePathsPlugin()
52 | ]
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Volume/Volume.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Volume.css';
3 | import VolumeOff from './../Icon/volume_off.svg';
4 | import VolumeUp from './../Icon/volume_up.svg';
5 |
6 | export default ({ onChange, onClick, volume, muted, className, ariaLabelMute, ariaLabelUnmute, ariaLabelVolume }) => {
7 | const volumeValue = muted || !volume
8 | ? 0
9 | : +volume;
10 | const isSilent = muted || volume <= 0;
11 | return (
12 |
16 |
35 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/PlayPause/PlayPause.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import PlayPause from './PlayPause';
4 | import styles from './PlayPause.css';
5 | import PlayArrow from './../Icon/play_arrow.svg';
6 | import Pause from './../Icon/pause.svg';
7 |
8 | describe('PlayPause', () => {
9 | let component;
10 |
11 | beforeAll(() => {
12 | component = shallow(
13 |
16 | );
17 | });
18 |
19 | it('should accept a className prop and append it to the components class', () => {
20 | const newClassNameString = 'a new className';
21 | expect(component.prop('className'))
22 | .toContain(styles.component);
23 | component.setProps({
24 | className: newClassNameString
25 | });
26 | expect(component.prop('className'))
27 | .toContain(styles.component);
28 | expect(component.prop('className'))
29 | .toContain(newClassNameString);
30 | });
31 |
32 | it('triggers \'onClick\' prop when the button is clicked', () => {
33 | const spy = jest.fn();
34 | component.setProps({
35 | onClick: spy
36 | });
37 | expect(spy)
38 | .not.toHaveBeenCalled();
39 | component.find('button').simulate('click');
40 | expect(spy)
41 | .toHaveBeenCalled();
42 | });
43 |
44 | describe('when paused', () => {
45 | beforeAll(() => {
46 | component.setProps({
47 | paused: true
48 | });
49 | });
50 |
51 | it('shows a play icon', () => {
52 | expect(component.html())
53 | .toContain(PlayArrow);
54 | expect(component.html())
55 | .not.toContain(Pause);
56 | });
57 |
58 | it('has correct aria-label', () => {
59 | expect(component.find('button').prop('aria-label'))
60 | .toEqual('Play');
61 | });
62 | });
63 |
64 | describe('when playing', () => {
65 | beforeAll(() => {
66 | component.setProps({
67 | paused: false
68 | });
69 | });
70 |
71 | it('shows a pause icon', () => {
72 | expect(component.html())
73 | .toContain(Pause);
74 | expect(component.html())
75 | .not.toContain(PlayArrow);
76 | });
77 |
78 | it('has correct aria-label', () => {
79 | expect(component.find('button').prop('aria-label'))
80 | .toEqual('Pause');
81 | });
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/demo/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { DefaultPlayer as Video } from 'react-html5video';
3 | import 'react-html5video/dist/styles.css';
4 | import styles from './App.css';
5 | import 'reset-css/reset.css';
6 | import vttEn from './../../assets/sintel-en.vtt';
7 | import vttEs from './../../assets/sintel-es.vtt';
8 | import bigBuckBunnyPoster from './../../assets/poster-big-buck-bunny.png';
9 | import sintelTrailerPoster from './../../assets/poster-sintel-trailer.png';
10 |
11 | const sintelTrailer = 'https://download.blender.org/durian/trailer/sintel_trailer-720p.mp4';
12 | const bigBuckBunny = 'http://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_h264.mov';
13 |
14 | class App extends Component {
15 | render () {
16 | return (
17 |
18 |
25 |
26 | -
27 |
47 |
48 | -
49 |
57 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | export default App;
65 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Time/Time.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Time from './Time';
4 | import styles from './Time.css';
5 |
6 | describe('Time', () => {
7 | let component;
8 |
9 | beforeAll(() => {
10 | component = shallow();
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 |
26 | it('shows video duration', () => {
27 | component.setProps({
28 | duration: 10
29 | });
30 | expect(component.find(`.${styles.duration}`).text())
31 | .toEqual('0:10');
32 | });
33 |
34 | it('shows current video elapsed time', () => {
35 | component.setProps({
36 | currentTime: 10
37 | });
38 | expect(component.find(`.${styles.current}`).text())
39 | .toEqual('0:10');
40 | });
41 |
42 | it('can handle minutes, hours and seconds', () => {
43 | component.setProps({
44 | currentTime: 60 * 2
45 | });
46 | expect(component.find(`.${styles.current}`).text())
47 | .toEqual('2:00');
48 |
49 | component.setProps({
50 | currentTime: 60 * 60 * 3
51 | });
52 | expect(component.find(`.${styles.current}`).text())
53 | .toEqual('3:00:00');
54 |
55 | component.setProps({
56 | currentTime: 60 * 60 * 3 + 72
57 | });
58 | expect(component.find(`.${styles.current}`).text())
59 | .toEqual('3:01:12');
60 | });
61 |
62 | it('fails gracefully and shows 00:00:00 when video is greater than 24 hours', () => {
63 | // Who has a video longer than 24 hours? If this ever occurs then we
64 | // should consider adding it.
65 | component.setProps({
66 | currentTime: 86401
67 | });
68 | expect(component.find(`.${styles.current}`).text())
69 | .toEqual('0:00');
70 |
71 | component.setProps({
72 | currentTime: 86400
73 | });
74 | expect(component.find(`.${styles.current}`).text())
75 | .toEqual('0:00');
76 | });
77 |
78 | it('fails gracefully and shows 00:00 when not given a number', () => {
79 | component.setProps({
80 | currentTime: null
81 | });
82 | expect(component.find(`.${styles.current}`).text())
83 | .toEqual('0:00');
84 |
85 | component.setProps({
86 | currentTime: undefined
87 | });
88 | expect(component.find(`.${styles.current}`).text())
89 | .toEqual('0:00');
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/Seek/Seek.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Seek from './Seek';
4 | import styles from './Seek.css';
5 |
6 | describe('Seek', () => {
7 | let component;
8 |
9 | beforeAll(() => {
10 | component = shallow();
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 |
26 | it('has a range input with correct ranges and percentagePlayed value', () => {
27 | component.setProps({
28 | percentagePlayed: 10
29 | });
30 | const rangeInput = component.find(`.${styles.input}`);
31 | expect(rangeInput.prop('type'))
32 | .toEqual('range');
33 | expect(rangeInput.prop('orient'))
34 | .toEqual('horizontal');
35 | expect(rangeInput.prop('step'))
36 | .toEqual(1);
37 | expect(rangeInput.prop('min'))
38 | .toEqual('0');
39 | expect(rangeInput.prop('max'))
40 | .toEqual('100');
41 | expect(rangeInput.prop('value'))
42 | .toEqual(10);
43 | });
44 |
45 | it('handles an undefined percentagePlayed value', () => {
46 | component.setProps({
47 | percentagePlayed: undefined
48 | });
49 | const rangeInput = component.find(`.${styles.input}`);
50 | expect(rangeInput.prop('value'))
51 | .toEqual(0);
52 | });
53 |
54 | it('triggers \'onChange\' prop when the input is changed', () => {
55 | const spy = jest.fn();
56 | component.setProps({
57 | onChange: spy
58 | });
59 | expect(spy)
60 | .not.toHaveBeenCalled();
61 | component.find('input').simulate('change');
62 | expect(spy)
63 | .toHaveBeenCalled();
64 | });
65 |
66 | it('changes the played fill bar width', () => {
67 | component.setProps({
68 | percentagePlayed: 0
69 | });
70 | expect(component.find(`.${styles.fill}`).prop('style').width)
71 | .toEqual('0%');
72 | component.setProps({
73 | percentagePlayed: 11
74 | });
75 | expect(component.find(`.${styles.fill}`).prop('style').width)
76 | .toEqual('11%');
77 | });
78 |
79 | it('changes the buffer bar width', () => {
80 | component.setProps({
81 | percentageBuffered: 0
82 | });
83 | expect(component.find(`.${styles.buffer}`).prop('style').width)
84 | .toEqual('0%');
85 | component.setProps({
86 | percentageBuffered: 11
87 | });
88 | expect(component.find(`.${styles.buffer}`).prop('style').width)
89 | .toEqual('11%');
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/src/video/api.js:
--------------------------------------------------------------------------------
1 | /**
2 | * These are custom helper methods that are not native
3 | * to the HTMLMediaElement API. Pass in the native
4 | * Video element, state and optional desired value to
5 | * set. To be primarily used in `mapVideoElToProps`.
6 | */
7 | export const togglePause = (videoEl, { paused }) => {
8 | if (paused) {
9 | videoEl.play();
10 | } else {
11 | videoEl.pause();
12 | }
13 | };
14 |
15 | export const setCurrentTime = (videoEl, state, value) => {
16 | videoEl.currentTime = value;
17 | };
18 |
19 | export const setVolume = (videoEl, state, value) => {
20 | videoEl.muted = false;
21 | videoEl.volume = value;
22 | };
23 |
24 | export const mute = (videoEl) => {
25 | videoEl.muted = true;
26 | };
27 |
28 | export const unmute = (videoEl) => {
29 | videoEl.muted = false;
30 | };
31 |
32 | export const toggleMute = (videoEl, { volume, muted }) => {
33 | if (muted || volume <= 0) {
34 | if (volume <= 0) {
35 | videoEl.volume = 1;
36 | }
37 | videoEl.muted = false;
38 | } else {
39 | videoEl.muted = true;
40 | }
41 | };
42 |
43 | export const toggleFullscreen = (videoEl) => {
44 | videoEl.requestFullScreen =
45 | videoEl.requestFullscreen
46 | || videoEl.msRequestFullscreen
47 | || videoEl.mozRequestFullScreen
48 | || videoEl.webkitRequestFullscreen;
49 | document.exitFullscreen =
50 | document.exitFullscreen
51 | || document.msExitFullscreen
52 | || document.mozCancelFullScreen
53 | || document.webkitExitFullscreen;
54 | const fullscreenElement =
55 | document.fullscreenElement
56 | || document.msFullscreenElement
57 | || document.mozFullScreenElement
58 | || document.webkitFullscreenElement;
59 | if (fullscreenElement === videoEl) {
60 | document.exitFullscreen();
61 | } else {
62 | videoEl.requestFullScreen();
63 | }
64 | };
65 |
66 | export const showTrack = ({ textTracks }, track) => {
67 | hideTracks({ textTracks });
68 | track.mode = track.SHOWING || 'showing';
69 | };
70 |
71 | export const hideTracks = ({ textTracks }) => {
72 | for (var i = 0; i < textTracks.length; i++) {
73 | textTracks[i].mode = textTracks[i].DISABLED || 'disabled';
74 | }
75 | };
76 |
77 | export const toggleTracks = (() => {
78 | let previousTrack;
79 | return ({ textTracks }) => {
80 | let currentTrack = [...textTracks]
81 | .filter((track) => track.mode === track.SHOWING || track.mode === 'showing')[0];
82 | if (currentTrack) {
83 | hideTracks({ textTracks });
84 | previousTrack = currentTrack;
85 | } else {
86 | showTrack({ textTracks }, previousTrack || textTracks[0]);
87 | }
88 | }})();
89 |
90 | /**
91 | * Custom getter methods that are commonly used
92 | * across video layouts. To be primarily used in
93 | * `mapStateToProps`
94 | */
95 | export const getPercentageBuffered = ({ buffered, duration }) =>
96 | buffered && buffered.length && buffered.end(buffered.length - 1) / duration * 100 || 0;
97 |
98 | export const getPercentagePlayed = ({ currentTime, duration }) =>
99 | currentTime / duration * 100;
100 |
--------------------------------------------------------------------------------
/demo/generateConfig.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const CopyWebpackPlugin = require('copy-webpack-plugin');
6 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
7 |
8 | const pkg = require('./../package.json');
9 | const srcPath = path.resolve(__dirname, 'src');
10 | const distPath = path.resolve(__dirname, 'dist');
11 |
12 | module.exports = ({ optimize, extractCss, hot, publicPath = '/' }) => {
13 | const cssString = 'css?modules&importLoaders=1&localIdentName=[hash:base64:5]&-autoprefixer!postcss';
14 | let config = {
15 | entry: [
16 | path.resolve(srcPath, 'entry.js')
17 | ],
18 | output: {
19 | path: distPath,
20 | filename: '[name].js',
21 | publicPath: publicPath
22 | },
23 | resolve: {
24 | extensions: ['.js', '.json', '.jsx', '']
25 | },
26 | module: {
27 | loaders: [{
28 | test: /\.(js|jsx)$/,
29 | include: srcPath,
30 | loader: 'babel',
31 | query: {
32 | cacheDirectory: true
33 | }
34 | }, {
35 | test: /\.json$/,
36 | loader: 'json'
37 | }, {
38 | test: /\.css$/,
39 | include: srcPath,
40 | loader: extractCss
41 | ? ExtractTextPlugin.extract('style', cssString)
42 | : 'style!' + cssString
43 | }, {
44 | test: /\.css$/,
45 | include: [new RegExp(pkg.name + '/dist/'), new RegExp('reset-css')],
46 | loader: extractCss
47 | ? ExtractTextPlugin.extract('style', 'css')
48 | : 'style!css'
49 | }, {
50 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|svg|vtt)(\?.*)?$/,
51 | loader: 'file'
52 | }]
53 | },
54 | plugins: [
55 | new HtmlWebpackPlugin({
56 | template: path.resolve(srcPath, 'index.html')
57 | }),
58 | new CopyWebpackPlugin([{
59 | context: 'assets/static',
60 | from: '**/*'
61 | }]),
62 | new CaseSensitivePathsPlugin()
63 | ]
64 | };
65 |
66 | if (hot) {
67 | config.entry.unshift(
68 | 'webpack-hot-middleware/client',
69 | 'react-hot-loader/patch'
70 | );
71 | config.plugins.unshift(
72 | new webpack.optimize.OccurenceOrderPlugin(),
73 | new webpack.HotModuleReplacementPlugin(),
74 | new webpack.NoErrorsPlugin()
75 | );
76 | }
77 |
78 | if (extractCss) {
79 | config.plugins.push(new ExtractTextPlugin('[name].css'));
80 | }
81 |
82 | if (optimize) {
83 | config.plugins.push(new webpack.optimize.UglifyJsPlugin({
84 | minimize: true,
85 | compress: {
86 | warnings: false
87 | },
88 | output: {
89 | comments: false
90 | }
91 | }));
92 | }
93 |
94 | return config;
95 | };
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-html5video",
3 | "version": "2.5.1",
4 | "description": "A customizeable HTML5 Video",
5 | "main": "dist",
6 | "files": [
7 | "dist"
8 | ],
9 | "scripts": {
10 | "start": "node scripts/start.js",
11 | "preinstall:demo": "npm run build",
12 | "install:demo": "cd demo && npm i",
13 | "i:demo": "npm run install:demo",
14 | "test": "jest --env=jsdom",
15 | "test:watch": "npm run test -- --watch",
16 | "build": "webpack --display-error-details",
17 | "build:demo": "cd demo && webpack",
18 | "deploy:demo": "node scripts/deploy:demo.js",
19 | "clean": "rm -rf node_modules && rm -rf dist && rm -rf demo/node_modules && rm -rf demo/dist",
20 | "lint": "eslint ./",
21 | "lint:fix": "npm run lint -- --fix",
22 | "preversion": "npm run clean && npm i && npm run i:demo && npm run lint && npm run test",
23 | "postversion": "git push && git push --tags"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/mderrick/react-html5video.git"
28 | },
29 | "keywords": [
30 | "react-component",
31 | "html5-video"
32 | ],
33 | "author": {
34 | "name": "Matt Derrick",
35 | "email": "me@mattderrick.co.uk",
36 | "url": "http://mattderrick.co.uk"
37 | },
38 | "license": "MIT",
39 | "jest": {
40 | "moduleFileExtensions": [
41 | "jsx",
42 | "js",
43 | "json"
44 | ],
45 | "moduleNameMapper": {
46 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico|vtt)$": "/__mocks__/fileMock.js",
47 | "^.+\\.css$": "identity-obj-proxy"
48 | },
49 | "transform": {
50 | "^.+\\.js$": "babel-jest",
51 | "^.+\\.(svg)$": "/__mocks__/fileTransformer.js"
52 | },
53 | "testPathIgnorePatterns": [
54 | "/(build|docs|node_modules)/"
55 | ],
56 | "testEnvironment": "node",
57 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(js|jsx)$"
58 | },
59 | "peerDependencies": {
60 | "react": ">=15.0.0",
61 | "react-dom": ">=15.0.0"
62 | },
63 | "devDependencies": {
64 | "babel": "^6.5.2",
65 | "babel-core": "^6.18.2",
66 | "babel-jest": "^18.0.0",
67 | "babel-loader": "^6.2.7",
68 | "babel-plugin-transform-object-rest-spread": "^6.19.0",
69 | "babel-plugin-transform-runtime": "^6.23.0",
70 | "babel-preset-es2015": "^6.18.0",
71 | "babel-preset-react": "^6.16.0",
72 | "babel-runtime": "^6.23.0",
73 | "case-sensitive-paths-webpack-plugin": "^1.1.4",
74 | "chalk": "^1.1.3",
75 | "copy-webpack-plugin": "^4.0.1",
76 | "css-loader": "^0.25.0",
77 | "detect-port": "^1.0.6",
78 | "enzyme": "^2.6.0",
79 | "eslint": "^3.11.1",
80 | "eslint-plugin-react": "^6.7.1",
81 | "express": "^4.14.0",
82 | "extract-text-webpack-plugin": "^1.0.1",
83 | "file-loader": "^0.9.0",
84 | "gh-pages": "^0.12.0",
85 | "html-webpack-plugin": "^2.24.1",
86 | "identity-obj-proxy": "^3.0.0",
87 | "jest": "^18.0.0",
88 | "json-loader": "^0.5.4",
89 | "open": "0.0.5",
90 | "postcss-loader": "^1.1.1",
91 | "prop-types": "^15.5.10",
92 | "react": "^15.4.0",
93 | "react-dom": "^15.4.0",
94 | "react-hot-loader": "^3.0.0-beta.6",
95 | "react-svg-loader": "^1.1.1",
96 | "react-test-renderer": "^15.5.4",
97 | "style-loader": "^0.13.1",
98 | "url-loader": "^0.5.7",
99 | "webpack": "^1.13.3",
100 | "webpack-dev-middleware": "^1.8.4",
101 | "webpack-hot-middleware": "^2.13.2"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/DefaultPlayer/DefaultPlayer.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { DefaultPlayer } from './DefaultPlayer';
4 | import styles from './DefaultPlayer.css';
5 | import Time from './Time/Time';
6 | import Seek from './Seek/Seek';
7 | import Volume from './Volume/Volume';
8 | import PlayPause from './PlayPause/PlayPause';
9 | import Fullscreen from './Fullscreen/Fullscreen';
10 | import Overlay from './Overlay/Overlay';
11 |
12 | describe('DefaultPlayer', () => {
13 | let component;
14 |
15 | beforeAll(() => {
16 | component = shallow();
17 | });
18 |
19 | it('should accept a className prop and append it to the components class', () => {
20 | const newClassNameString = 'a new className';
21 | expect(component.prop('className'))
22 | .toContain(styles.component);
23 | component.setProps({
24 | className: newClassNameString
25 | });
26 | expect(component.prop('className'))
27 | .toContain(styles.component);
28 | expect(component.prop('className'))
29 | .toContain(newClassNameString);
30 | });
31 |
32 | it('applies `style` prop if provided', () => {
33 | component.setProps({
34 | style: { color: 'red' }
35 | });
36 | expect(component.prop('style'))
37 | .toEqual({ color: 'red' });
38 | });
39 |
40 | it('spreads all parent props on the video element', () => {
41 | component.setProps({
42 | autoPlay: true
43 | });
44 | expect(component.find('video').prop('autoPlay'))
45 | .toEqual(true);
46 | });
47 |
48 | it('has an overlay component', () => {
49 | expect(component.find(Overlay).exists())
50 | .toBeTruthy();
51 | });
52 |
53 | it('renders some default controls in a default order', () => {
54 | const controlsComponent = component.find(`.${styles.controls}`);
55 | expect(controlsComponent.childAt(0).is(PlayPause))
56 | .toBeTruthy();
57 | expect(controlsComponent.childAt(1).is(Seek))
58 | .toBeTruthy();
59 | expect(controlsComponent.childAt(2).is(Time))
60 | .toBeTruthy();
61 | expect(controlsComponent.childAt(3).is(Volume))
62 | .toBeTruthy();
63 | expect(controlsComponent.childAt(4).is(Fullscreen))
64 | .toBeTruthy();
65 | });
66 |
67 | it('renders controls in a given custom order', () => {
68 | component.setProps({
69 | controls: ['Fullscreen', 'Seek', 'Time']
70 | });
71 | const controlsComponent = component.find(`.${styles.controls}`);
72 | expect(controlsComponent.childAt(0).is(Fullscreen))
73 | .toBeTruthy();
74 | expect(controlsComponent.childAt(1).is(Seek))
75 | .toBeTruthy();
76 | expect(controlsComponent.childAt(2).is(Time))
77 | .toBeTruthy();
78 | });
79 |
80 | it('renders no controls when given an empty array', () => {
81 | expect(component.find(`.${styles.controls}`).exists())
82 | .toBeTruthy();
83 | component.setProps({
84 | controls: []
85 | });
86 | expect(component.find(`.${styles.controls}`).exists())
87 | .toBeFalsy();
88 | });
89 |
90 | it('renders no controls when there is an error', () => {
91 | component.setProps({
92 | controls: ['PlayPause'],
93 | video: {
94 | error: false
95 | }
96 | });
97 | expect(component.find(`.${styles.controls}`).exists())
98 | .toBeTruthy();
99 | component.setProps({
100 | controls: ['PlayPause'],
101 | video: {
102 | error: true
103 | }
104 | });
105 | expect(component.find(`.${styles.controls}`).exists())
106 | .toBeFalsy();
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/src/video/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a HoC that finds a single
3 | *