├── .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 | 
4 | [](#contributors-)
5 | 
6 | 
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 |
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 ;
84 |
85 | // Fallback to using a regular video player if HLS is supported by default in the user's browser
86 | return ;
87 | }
88 |
89 | export default ReactHlsPlayer;
90 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist",
4 | "target": "ES5",
5 | "module": "ES6",
6 | "declaration": true,
7 | "esModuleInterop": true,
8 | "moduleResolution": "node",
9 | "removeComments": true,
10 | "jsx": "react",
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "allowJs": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": true
16 | },
17 | "exclude": ["node_modules", "./dist"],
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | rules: [
4 | {
5 | test: /\.ts?x$/,
6 | use: 'ts-loader',
7 | exclude: /node_modules/,
8 | },
9 | ],
10 | },
11 | resolve: {
12 | extensions: ['.tsx', '.ts', '.js', '.jsx'],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | const config = require('./webpack.config');
5 |
6 | module.exports = merge(config, {
7 | mode: 'development',
8 | entry: './example/index.tsx',
9 | devtool: 'inline-source-map',
10 | devServer: {
11 | port: 8000,
12 | open: true,
13 | historyApiFallback: true,
14 | contentBase: 'public',
15 | host: '0.0.0.0',
16 | public: 'localhost:8000',
17 | },
18 | plugins: [
19 | new HtmlWebpackPlugin({
20 | filename: 'index.html',
21 | template: 'public/index.html',
22 | }),
23 | ],
24 | });
25 |
--------------------------------------------------------------------------------