├── .gitignore
├── .npmignore
├── src
├── video-source.jsx
├── button.jsx
├── navigate.jsx
├── play-pause.jsx
├── fullscreen-controls.jsx
├── viewport.jsx
├── react-a11y-video.jsx
├── volume-controls.jsx
├── progress-controls.jsx
├── playback-controls.jsx
├── playback-time.jsx
├── control-panel.jsx
├── styles
│ └── react-a11y-video.scss
└── img
│ └── fullscreen-icon.svg
├── example
├── index.html
└── main.jsx
├── dev-server.js
├── .editorconfig
├── README.md
├── webpack.config.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | dev-server.js
3 | webpack.config.js
4 | .editorconfig
5 |
--------------------------------------------------------------------------------
/src/video-source.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let VideoSource = React.createClass({
4 |
5 | render() {
6 |
7 | return ;
8 | }
9 | });
10 |
11 | export default VideoSource;
12 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React A11y HTML5 Video Player
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import A11yPlayer from '../src/react-a11y-video';
4 |
5 | let sources = [
6 | 'http://www.html5rocks.com/en/tutorials/video/basics/devstories.webm'
7 | ];
8 |
9 | let props = {
10 |
11 | sources,
12 | width: 400
13 | };
14 |
15 | React.render(, document.body);
16 |
--------------------------------------------------------------------------------
/src/button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let Button = React.createClass({
4 |
5 | render() {
6 |
7 | return (
8 |
9 |
14 | );
15 | }
16 | });
17 |
18 | export default Button;
19 |
--------------------------------------------------------------------------------
/dev-server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var config = require('./webpack.config');
4 |
5 | new WebpackDevServer(webpack(config), {
6 |
7 | publicPath: config.output.publicPath,
8 | hot: true
9 | })
10 | .listen(config.port, config.address, function (err, result) {
11 |
12 | if (err) { console.log(err); }
13 |
14 | console.log('Listening at http://' + config.address + ':' + config.port);
15 | });
16 |
--------------------------------------------------------------------------------
/src/navigate.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './button';
4 |
5 | let Navigate = React.createClass({
6 |
7 | render() {
8 |
9 | return (
10 |
11 |
12 |
13 |
17 |
18 |
19 | );
20 | }
21 | });
22 |
23 | export default Navigate;
24 |
--------------------------------------------------------------------------------
/src/play-pause.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './button';
4 |
5 | let PlayPause = React.createClass({
6 |
7 | render() {
8 |
9 | return (
10 |
11 |
12 |
13 |
17 |
18 |
19 | );
20 | }
21 | });
22 |
23 | export default PlayPause;
24 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 4
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/src/fullscreen-controls.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './button';
4 |
5 | let FullscreenControls = React.createClass({
6 |
7 | _fullscreen() {
8 |
9 | this.props.api.webkitRequestFullscreen();
10 | },
11 |
12 | render() {
13 |
14 | return (
15 |
16 |
17 |
18 |
22 |
23 |
24 | );
25 | }
26 | });
27 |
28 | export default FullscreenControls;
29 |
--------------------------------------------------------------------------------
/src/viewport.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import VideoSource from './video-source';
4 |
5 | let Viewport = React.createClass({
6 |
7 | render() {
8 |
9 | let sources = this.props.sources
10 | .map((src, index) => );
11 |
12 | return (
13 |
14 |
21 | );
22 | }
23 | });
24 |
25 | export default Viewport;
26 |
--------------------------------------------------------------------------------
/src/react-a11y-video.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Viewport from './viewport';
4 | import ControlPanel from './control-panel';
5 |
6 | require('./styles/react-a11y-video');
7 |
8 | let A11yPlayer = React.createClass({
9 |
10 | getInitialState() {
11 |
12 | return {
13 |
14 | api: undefined
15 | };
16 | },
17 |
18 | componentDidMount() {
19 |
20 | this._initApi();
21 | },
22 |
23 | _initApi() {
24 |
25 | this.setState({ api: this.refs.viewport.refs.api.getDOMNode() });
26 | },
27 |
28 | render() {
29 |
30 | return (
31 |
32 |
34 |
35 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 | });
46 |
47 | export default A11yPlayer;
48 |
--------------------------------------------------------------------------------
/src/volume-controls.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let VolumeControls = React.createClass({
4 |
5 | getInitialState() {
6 |
7 | return {
8 |
9 | level: 5
10 | }
11 | },
12 |
13 | componentWillReceiveProps (nextProps) {
14 |
15 | this.props.api !== nextProps.api &&
16 |
17 | this._setVolume(this.state.level);
18 | },
19 |
20 | _onChange ({ target: { value }}) {
21 |
22 | this._setVolume(value);
23 | },
24 |
25 | _setVolume (level) {
26 |
27 | this.setState({ level }, () => {
28 |
29 | this.props.api.volume = level / 10;
30 | });
31 | },
32 |
33 | render() {
34 |
35 | return (
36 |
37 |
38 |
39 |
45 |
46 |
47 | );
48 | }
49 | });
50 |
51 | export default VolumeControls;
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://badge.fury.io/js/react-a11y-video)
2 | 
3 |
4 | # React A11y HTML5 Video Player
5 |
6 | Accessible HTML5 Video player React component.
7 |
8 | ## WIP
9 |
10 | This is WIP, check [Todo](#todo). More features will come soon.
11 |
12 | ## Installation
13 |
14 | This component requires `webpack`. Check [`webpack.config.js`](./webpack.config.js) for build configuration.
15 |
16 | `npm install react-a11y-video`
17 |
18 | ## Usage
19 |
20 | Check [`examples/main.jsx`](https://github.com/roman01la/react-a11y-video/blob/master/example/main.jsx).
21 |
22 | `import A11yPlayer from 'react-a11y-video';`
23 |
24 | Basically pass video **source urls** and player **dimensions** as props to `A11yPlayer` component.
25 |
26 | ## Development
27 |
28 | `npm install && npm start`
29 |
30 | Go to [localhost:3000/example/](http://localhost:3000/example/)
31 |
32 | ## Todo
33 |
34 | - Better keyboard accessibility in fullscreen mode
35 | - Playback rate controls
36 | - Subtitles support
37 | - Invert colors and grayscale modes for both UI and video content
38 | - Show transcript option
39 | - Responsiveness
40 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | const ADDRESS = '0.0.0.0';
4 | const PORT = 3000;
5 |
6 | module.exports = {
7 |
8 | address: ADDRESS,
9 | port: PORT,
10 | devtool: 'source-map',
11 | watch: true,
12 | entry: [
13 | 'webpack-dev-server/client?http://' + ADDRESS + ':' + PORT,
14 | 'webpack/hot/only-dev-server',
15 | __dirname + '/src/react-a11y-video.jsx',
16 | __dirname + '/example/main.jsx'
17 | ],
18 | module: {
19 | loaders: [
20 | { test: /\.(jsx|es6)$/, exclude: /node_modules/, loaders: ['react-hot', '6to5-loader?optional=selfContained'] },
21 | { test: /\.scss$/, loaders: ['react-hot', 'style-loader', 'css-loader', 'autoprefixer-loader', 'sass-loader'] },
22 | { test: /\.svg$/, loaders: ['url-loader?limit=100000&mimetype=image/svg+xml'] }
23 | ]
24 | },
25 | output: {
26 | path: __dirname + '/build',
27 | filename: 'main.js',
28 | publicPath: '/'
29 | },
30 | resolve: {
31 | extensions: ['', '.js', '.jsx', '.es6', '.scss']
32 | },
33 | plugins: [
34 | new webpack.HotModuleReplacementPlugin(),
35 | new webpack.NoErrorsPlugin()
36 | ]
37 | };
38 |
--------------------------------------------------------------------------------
/src/progress-controls.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let ProgressControls = React.createClass({
4 |
5 | getInitialState() {
6 |
7 | return {
8 |
9 | progress: 0
10 | };
11 | },
12 |
13 | componentWillReceiveProps (nextProps) {
14 |
15 | this.props.api !== nextProps.api &&
16 |
17 | nextProps.api
18 | .addEventListener('timeupdate', this._onTimeUpdate, false);
19 | },
20 |
21 | _onTimeUpdate() {
22 |
23 | let api = this.props.api,
24 | progress = (100 / api.duration) * api.currentTime;
25 |
26 | this.setState({ progress });
27 | },
28 |
29 | _seek (event) {
30 |
31 | let api = this.props.api,
32 | pos = (event.pageX - event.target.offsetLeft) / event.target.offsetWidth;
33 |
34 | api.currentTime = pos * api.duration;
35 | },
36 |
37 | render() {
38 |
39 | return (
40 |
41 |
48 | );
49 | }
50 | });
51 |
52 | export default ProgressControls;
53 |
--------------------------------------------------------------------------------
/src/playback-controls.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import PlayPause from './play-pause';
4 | import Navigate from './navigate';
5 |
6 | let PlaybackControls = React.createClass({
7 |
8 | getInitialState() {
9 |
10 | return {
11 |
12 | playing: false
13 | };
14 | },
15 |
16 | _onPlayPause() {
17 |
18 | this.setState({ playing: !this.state.playing }, () => {
19 |
20 | this.props[this.state.playing ? 'onPlay' : 'onPause']();
21 | });
22 | },
23 |
24 | _onRewind() {
25 |
26 | this.props.onNavigate(false);
27 | },
28 |
29 | _onFastForward() {
30 |
31 | this.props.onNavigate(true);
32 | },
33 |
34 | render() {
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 | Repeat
43 |
44 |
45 |
46 |
47 | Back
48 |
49 |
50 |
52 |
53 |
54 |
55 | Forward
56 |
57 |
58 |
59 | );
60 | }
61 | });
62 |
63 | export default PlaybackControls;
64 |
--------------------------------------------------------------------------------
/src/playback-time.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let PlaybackTime = React.createClass({
4 |
5 | getInitialState() {
6 |
7 | return {
8 |
9 | currTime: '00:00',
10 | duration: 0
11 | };
12 | },
13 |
14 | componentWillReceiveProps (nextProps) {
15 |
16 | if (this.props.api !== nextProps.api) {
17 |
18 | nextProps.api
19 | .addEventListener('timeupdate', this._onTimeUpdate, false);
20 |
21 | let check = setInterval(() => {
22 |
23 | if (nextProps.api.duration > 0) {
24 |
25 | this.setState({ duration: nextProps.api.duration });
26 | clearInterval(check);
27 | }
28 |
29 | }, 1000);
30 | }
31 | },
32 |
33 | _onTimeUpdate() {
34 |
35 | let api = this.props.api;
36 |
37 | let mins = this._formatTime(parseInt((api.currentTime / 60) % 60)),
38 | secs = this._formatTime(parseInt(api.currentTime % 60));
39 |
40 | this.setState({ currTime: mins + ':' + secs});
41 | },
42 |
43 | _formatTime (time) {
44 |
45 | return ('0' + time).slice(-2);
46 | },
47 |
48 | render() {
49 |
50 | return (
51 |
52 |
55 |
56 | {this.state.currTime}
57 |
58 | );
59 | }
60 | });
61 |
62 | export default PlaybackTime;
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-a11y-video",
3 | "version": "1.1.0",
4 | "description": "Accessible HTML5 Video player React component",
5 | "homepage": "https://github.com/roman01la/react-a11y-video",
6 | "main": "build/react-a11y-video.js",
7 | "scripts": {
8 | "start": "node dev-server.js",
9 | "prepublish": "mkdir -p build && 6to5 src/ -d build/ && find build -type f \\( -iname \\*.jsx -o -iname \\*.es6 \\) -exec sh -c 'mv \"$1\" \"${1%.jsx}.js\"' _ {} \\; && cp -r src/img build/ && cp -r src/styles build/"
10 | },
11 | "author": {
12 | "name": "Roman Liutikov",
13 | "email": "roman01la@romanliutikov.com",
14 | "url": "https://romanliutikov.com"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/roman01la/react-a11y-video.git"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/roman01la/react-a11y-video/issues"
22 | },
23 | "keywords": [
24 | "react",
25 | "reactjs",
26 | "a11y",
27 | "react-component",
28 | "video",
29 | "player",
30 | "accessible",
31 | "html5"
32 | ],
33 | "license": "MIT",
34 | "peerDependencies": {
35 | "react": "^0.12.2"
36 | },
37 | "devDependencies": {
38 | "6to5": "^3.4.1",
39 | "6to5-core": "^3.4.1",
40 | "6to5-loader": "^3.0.0",
41 | "6to5-runtime": "^3.4.1",
42 | "autoprefixer-loader": "^1.1.0",
43 | "core-js": "^0.4.10",
44 | "css-loader": "^0.9.1",
45 | "file-loader": "^0.8.1",
46 | "react-hot-loader": "^1.1.4",
47 | "sass-loader": "^0.4.0-beta.1",
48 | "style-loader": "^0.8.3",
49 | "url-loader": "^0.5.5",
50 | "webpack": "^1.5.3",
51 | "webpack-dev-server": "^1.7.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/control-panel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import PlaybackControls from './playback-controls';
4 | import VolumeControls from './volume-controls';
5 | import PlaybackTime from './playback-time';
6 | import FullscreenControls from './fullscreen-controls';
7 | import ProgressControls from './progress-controls';
8 |
9 | let ControlPanel = React.createClass({
10 |
11 | getDefaultProps() {
12 |
13 | return {
14 |
15 | api: undefined,
16 | navStep: 10
17 | };
18 | },
19 |
20 | _play() {
21 |
22 | this.props.api.play();
23 | },
24 |
25 | _pause() {
26 |
27 | this.props.api.pause();
28 | },
29 |
30 | _navigate (dir) {
31 |
32 | let currTime = this.props.api.currentTime,
33 | navStep = this.props.navStep;
34 |
35 | this.props.api.currentTime = currTime + (dir ? navStep : (-navStep));
36 | },
37 |
38 | _repeat() {
39 |
40 | this.props.api.currentTime = 0;
41 | },
42 |
43 | render() {
44 |
45 | return (
46 |
47 |
48 |
49 |
58 |
59 |
63 |
64 |
65 | );
66 | }
67 | });
68 |
69 | export default ControlPanel;
70 |
--------------------------------------------------------------------------------
/src/styles/react-a11y-video.scss:
--------------------------------------------------------------------------------
1 | .react-a11y-video {
2 |
3 | $colorCtrl: #242424;
4 | $colorWhite: #fafafa;
5 | $colorCtrlInteract: lighten($colorCtrl, 20%);
6 | $colorCtrlSecondary: lighten($colorCtrl, 60%);
7 |
8 | background: $colorWhite;
9 | border: 1px solid $colorCtrl;
10 | padding: 5px;
11 | border-radius: 2px;
12 |
13 | video {
14 | border-radius: 2px;
15 | }
16 |
17 | .control-panel {
18 | flex-direction: column;
19 | }
20 |
21 | .control-panel,
22 | .playback-controls,
23 | .row {
24 | display: flex;
25 | }
26 |
27 | .row {
28 |
29 | &.top {
30 | justify-content: space-between;
31 | }
32 | &.bottom {
33 | padding: 5px 0 0;
34 | }
35 | }
36 |
37 | .btn,
38 | .playback-time {
39 | margin: 2px;
40 | padding: 4px 8px;
41 | border-radius: 2px;
42 | font: normal 12px sans-serif;
43 | line-height: 1rem;
44 | }
45 |
46 | .btn {
47 | border: none;
48 | background: $colorCtrl;
49 | color: $colorWhite;
50 |
51 | &:hover {
52 | cursor: pointer;
53 | }
54 |
55 | &:hover,
56 | &:focus {
57 | background: $colorCtrlInteract;
58 | }
59 | }
60 |
61 | .volume-controls {
62 | display: flex;
63 | align-items: center;
64 |
65 | input[type='range'] {
66 | -webkit-appearance: none;
67 | height: 6px;
68 | width: 100px;
69 | background: $colorCtrlSecondary;
70 |
71 | &::-webkit-slider-thumb {
72 | -webkit-appearance: none;
73 | height: 10px;
74 | width: 6px;
75 | background: $colorCtrl;
76 | }
77 | }
78 | }
79 |
80 | .progress-controls,
81 | .fullscreen-controls {
82 | flex: 1;
83 | }
84 |
85 | .progress-controls progress {
86 | -webkit-appearance: none;
87 | border: none;
88 | width: 100%;
89 | height: 10px;
90 |
91 | &::-webkit-progress-bar {
92 | background: $colorCtrlSecondary;
93 | }
94 | &::-webkit-progress-value {
95 | background: $colorCtrl;
96 | }
97 | }
98 |
99 | .fullscreen-controls {
100 | margin: 0 0 0 10px;
101 | max-width: 26px;
102 |
103 | .btn {
104 | width: 26px;
105 | height: 26px;
106 | font-size: 0;
107 | display: block;
108 | background: url(../img/fullscreen-icon.svg);
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/img/fullscreen-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------