├── .all-contributorsrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── example ├── App │ └── App.tsx └── index.tsx ├── package-lock.json ├── package.json ├── public └── index.html ├── src └── index.tsx ├── tsconfig.json └── webpack ├── webpack.config.js └── webpack.dev.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "mxrcochxvez", 10 | "name": "Marco Chavez", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/43889446?v=4", 12 | "profile": "https://www.marcochavez.info/", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "devcshort", 19 | "name": "Chris Short", 20 | "avatar_url": "https://avatars3.githubusercontent.com/u/13677134?v=4", 21 | "profile": "https://www.chrisrshort.com", 22 | "contributions": [ 23 | "code", 24 | "projectManagement" 25 | ] 26 | } 27 | ], 28 | "contributorsPerLine": 7, 29 | "projectName": "react-hls", 30 | "projectOwner": "devcshort", 31 | "repoType": "github", 32 | "repoHost": "https://github.com", 33 | "skipCi": true 34 | } 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "plugin:react/recommended", 13 | "plugin:jsx-a11y/recommended", 14 | "plugin:prettier/recommended", 15 | "prettier" 16 | ], 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": 2020, 22 | "sourceType": "module" 23 | }, 24 | "plugins": ["react", "react-hooks", "@typescript-eslint"], 25 | "settings": { 26 | "react": { 27 | "version": "detect" 28 | } 29 | }, 30 | "rules": { 31 | "react/prefer-stateless-function": "off", 32 | "react/prop-types": 0, 33 | "react-hooks/exhaustive-deps": "warn", 34 | "jsx-a11y/media-has-caption": "off", 35 | "prettier/prettier": [ 36 | "error", 37 | { 38 | "endOfLine": "crlf" 39 | } 40 | ], 41 | "no-irregular-whitespace": "off", 42 | "indent": ["error", 2, { "SwitchCase": 1 }], 43 | "linebreak-style": ["error", "windows"], 44 | "semi": ["error", "always"], 45 | "eol-last": ["error", "always"], 46 | "quotes": ["error", "single"], 47 | "prefer-const": [ 48 | "error", 49 | { 50 | "destructuring": "any", 51 | "ignoreReadBeforeAssign": false 52 | } 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .all-contributorsrc 2 | .eslintrc 3 | .prettierrc 4 | tsconfig.json 5 | typings/ 6 | webpack/ 7 | src/ 8 | public/ 9 | node_modules/ 10 | example/ 11 | .vscode/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.wordWrap": "on" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christopher Short 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React HLS Player 2 | 3 | ![NPM Downloads](https://img.shields.io/npm/dm/react-hls-player?style=flat-square) 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 5 | ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/react-hls-player) 6 | ![npm bundle size](https://img.shields.io/bundlephobia/min/react-hls-player) 7 | 8 | ## Introduction 9 | 10 | `react-hls-player` is a simple HLS live stream player. 11 | It uses [hls.js](https://github.com/video-dev/hls.js) to play your hls live stream if your browser supports `html 5 video` and `MediaSource Extension`. 12 | 13 | ## Examples 14 | 15 | ### Using the ReactHlsPlayer component 16 | 17 | ```javascript 18 | import React from 'react'; 19 | import ReactDOM from 'react-dom'; 20 | import ReactHlsPlayer from 'react-hls-player'; 21 | 22 | ReactDOM.render( 23 | , 30 | document.getElementById('app') 31 | ); 32 | ``` 33 | 34 | ### Using hlsConfig (advanced use case) 35 | 36 | All available config properties can be found on the [Fine Tuning](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) section of the Hls.js API.md 37 | 38 | ```javascript 39 | import React from 'react'; 40 | import ReactDOM from 'react-dom'; 41 | import ReactHlsPlayer from 'react-hls-player'; 42 | 43 | ReactDOM.render( 44 | , 52 | document.getElementById('app') 53 | ); 54 | ``` 55 | 56 | ### Using playerRef 57 | 58 | The `playerRef` returns a ref to the underlying video component, and as such will give you access to all video component properties and methods. 59 | 60 | ```javascript 61 | import React from 'react'; 62 | import ReactHlsPlayer from 'react-hls-player'; 63 | 64 | function MyCustomComponent() { 65 | const playerRef = React.useRef(); 66 | 67 | function playVideo() { 68 | playerRef.current.play(); 69 | } 70 | 71 | function pauseVideo() { 72 | playerRef.current.pause(); 73 | } 74 | 75 | function toggleControls() { 76 | playerRef.current.controls = !playerRef.current.controls; 77 | } 78 | 79 | return ( 80 | 84 | ); 85 | } 86 | 87 | ReactDOM.render(, document.getElementById('app')); 88 | ``` 89 | 90 | You can also listen to events of the video 91 | 92 | ```javascript 93 | import React from 'react'; 94 | import ReactHlsPlayer from 'react-hls-player'; 95 | 96 | function MyCustomComponent() { 97 | const playerRef = React.useRef(); 98 | 99 | React.useEffect(() => { 100 | function fireOnVideoStart() { 101 | // Do some stuff when the video starts/resumes playing 102 | } 103 | 104 | playerRef.current.addEventListener('play', fireOnVideoStart); 105 | 106 | return playerRef.current.removeEventListener('play', fireOnVideoStart); 107 | }, []); 108 | 109 | React.useEffect(() => { 110 | function fireOnVideoEnd() { 111 | // Do some stuff when the video ends 112 | } 113 | 114 | playerRef.current.addEventListener('ended', fireOnVideoEnd); 115 | 116 | return playerRef.current.removeEventListener('ended', fireOnVideoEnd); 117 | }, []); 118 | 119 | return ( 120 | 124 | ); 125 | } 126 | 127 | ReactDOM.render(, document.getElementById('app')); 128 | ``` 129 | 130 | ## Props 131 | 132 | All [video properties](https://www.w3schools.com/tags/att_video_poster.asp) are supported and passed down to the underlying video component 133 | 134 | | Prop | Description | 135 | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | 136 | | src `String`, `required` | The hls url that you want to play | 137 | | autoPlay `Boolean` | Autoplay when component is ready. Defaults to `false` | 138 | | controls `Boolean` | Whether or not to show the playback controls. Defaults to `false` | 139 | | width `Number` | Video width. Defaults to `100%` | 140 | | height `Number` | Video height. Defaults to `auto` | 141 | | hlsConfig `Object` | `hls.js` config, you can see all config [here](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) | 142 | | playerRef `React Ref` | Pass in your own ref to interact with the video player directly. This will override the default ref. | 143 | 144 | ### Additional Notes 145 | 146 | By default, the HLS config will have `enableWorker` set to `false`. There have been issues with the HLS.js library that breaks some React apps, so I've disabled it to prevent people from running in to this issue. If you want to enable it and see if it works with your React app, you can simply pass in `enableWorker: true` to the `hlsConfig` prop object. [See this issue for more information](https://github.com/video-dev/hls.js/issues/2064) 147 | 148 | ## Contributors ✨ 149 | 150 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |

Marco Chavez

💻

Chris Short

💻 📆
161 | 162 | 163 | 164 | 165 | 166 | 167 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 168 | -------------------------------------------------------------------------------- /example/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | 3 | import HlsPlayer from '../../src'; 4 | 5 | function App() { 6 | const playerRef = useRef(null); 7 | const inputRef = useRef(null); 8 | const [hlsUrl, setHlsUrl] = useState( 9 | 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8' 10 | ); 11 | const [destroy, setDestroy] = useState(false); 12 | 13 | function _handleEnter(e: React.KeyboardEvent) { 14 | if (e.keyCode === 13) { 15 | setHlsUrl(inputRef?.current?.value ?? ''); 16 | } 17 | } 18 | 19 | function _handleDestroyClick() { 20 | setDestroy(true); 21 | } 22 | 23 | function _handleToggleControls() { 24 | if (playerRef?.current?.hasAttribute('controls')) { 25 | playerRef.current.removeAttribute('controls'); 26 | } else { 27 | playerRef?.current?.setAttribute('controls', 'true'); 28 | } 29 | } 30 | 31 | return ( 32 |
33 |
38 | 47 | 61 |
62 | 63 | {!destroy ? ( 64 | 72 | ) : null} 73 | 74 |
75 | 76 | 84 | 85 | 93 |
94 | ); 95 | } 96 | 97 | export default App; 98 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App/App'; 5 | 6 | ReactDOM.render(, document.getElementById('container')); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hls-player", 3 | "version": "3.0.7", 4 | "description": "A simple and easy to use react component for playing an hls live stream", 5 | "main": "./dist/index.js", 6 | "types": "./dist", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "webpack serve --config webpack/webpack.dev.js", 10 | "build": "tsc", 11 | "prepublishOnly": "npm run build", 12 | "prepare": "install-peers" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/devcshort/react-hls.git" 17 | }, 18 | "keywords": [ 19 | "hls", 20 | "rtmp", 21 | "react", 22 | "component" 23 | ], 24 | "author": "Christopher Short", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/devcshort/react-hls/issues" 28 | }, 29 | "homepage": "https://github.com/devcshort/react-hls#README", 30 | "dependencies": { 31 | "hls.js": "^0.14.17" 32 | }, 33 | "peerDependencies": { 34 | "react": "^16.13.1", 35 | "react-dom": "^16.13.1" 36 | }, 37 | "devDependencies": { 38 | "@types/hls.js": "^0.13.3", 39 | "@types/react": "^17.0.3", 40 | "@types/react-dom": "^17.0.2", 41 | "@typescript-eslint/eslint-plugin": "^4.18.0", 42 | "@typescript-eslint/parser": "^4.18.0", 43 | "eslint": "^7.4.0", 44 | "eslint-config-prettier": "^8.1.0", 45 | "eslint-plugin-import": "^2.22.1", 46 | "eslint-plugin-jsx": "^0.1.0", 47 | "eslint-plugin-jsx-a11y": "^6.3.1", 48 | "eslint-plugin-prettier": "^3.3.1", 49 | "eslint-plugin-react": "^7.20.3", 50 | "eslint-plugin-react-hooks": "^4.0.8", 51 | "html-webpack-plugin": "^5.3.1", 52 | "install-peers-cli": "^2.2.0", 53 | "prettier": "^2.2.1", 54 | "ts-loader": "^8.0.18", 55 | "typescript": "^4.2.3", 56 | "webpack": "^5.26.3", 57 | "webpack-cli": "^4.5.0", 58 | "webpack-dev-server": "^3.11.0", 59 | "webpack-merge": "^5.0.9" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | react-hls-player 7 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, RefObject } from 'react'; 2 | import Hls, { Config } from 'hls.js'; 3 | 4 | export interface HlsPlayerProps 5 | extends React.VideoHTMLAttributes { 6 | hlsConfig?: Config; 7 | playerRef: RefObject; 8 | src: string; 9 | } 10 | 11 | function ReactHlsPlayer({ 12 | hlsConfig, 13 | playerRef = React.createRef(), 14 | src, 15 | autoPlay, 16 | ...props 17 | }: HlsPlayerProps) { 18 | useEffect(() => { 19 | let hls: Hls; 20 | 21 | function _initPlayer() { 22 | if (hls != null) { 23 | hls.destroy(); 24 | } 25 | 26 | const newHls = new Hls({ 27 | enableWorker: false, 28 | ...hlsConfig, 29 | }); 30 | 31 | if (playerRef.current != null) { 32 | newHls.attachMedia(playerRef.current); 33 | } 34 | 35 | newHls.on(Hls.Events.MEDIA_ATTACHED, () => { 36 | newHls.loadSource(src); 37 | 38 | newHls.on(Hls.Events.MANIFEST_PARSED, () => { 39 | if (autoPlay) { 40 | playerRef?.current 41 | ?.play() 42 | .catch(() => 43 | console.log( 44 | 'Unable to autoplay prior to user interaction with the dom.' 45 | ) 46 | ); 47 | } 48 | }); 49 | }); 50 | 51 | newHls.on(Hls.Events.ERROR, function (event, data) { 52 | if (data.fatal) { 53 | switch (data.type) { 54 | case Hls.ErrorTypes.NETWORK_ERROR: 55 | newHls.startLoad(); 56 | break; 57 | case Hls.ErrorTypes.MEDIA_ERROR: 58 | newHls.recoverMediaError(); 59 | break; 60 | default: 61 | _initPlayer(); 62 | break; 63 | } 64 | } 65 | }); 66 | 67 | hls = newHls; 68 | } 69 | 70 | // Check for Media Source support 71 | if (Hls.isSupported()) { 72 | _initPlayer(); 73 | } 74 | 75 | return () => { 76 | if (hls != null) { 77 | hls.destroy(); 78 | } 79 | }; 80 | }, [autoPlay, hlsConfig, playerRef, src]); 81 | 82 | // If Media Source is supported, use HLS.js to play video 83 | if (Hls.isSupported()) return