├── .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 | [![npm version](https://badge.fury.io/js/react-a11y-video.svg)](http://badge.fury.io/js/react-a11y-video) 2 | ![](https://img.shields.io/badge/maintainer%20needed-!-red.svg) 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 |
42 | 43 | 46 | 47 |
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 |
50 | 54 | 55 | 56 | 57 |
58 | 59 |
60 | 61 | 62 |
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 | 3 | 4 | Slice 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------