├── .prettierignore ├── .prettierrc.js ├── README.md ├── dist ├── RTSPtoWEBPlayer.js └── index.html ├── examples ├── ReactComponent │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── control-btn.js │ │ ├── images │ │ │ ├── pause.svg │ │ │ └── play.svg │ │ ├── input-group.js │ │ └── react-player │ │ │ ├── react-player.css │ │ │ └── react-player.js │ │ └── index.js └── Vue3Component │ ├── README.md │ ├── babel.config.js │ ├── jsconfig.json │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.vue │ ├── components │ │ └── PlayerVue.vue │ └── main.js │ └── vue.config.js ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── rtsp-to-web-player.css └── rtsp-to-web-player.js └── webpack.config.js /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | examples 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | bracketSpacing: false, 4 | useTabs: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | arrowParens: 'avoid', 8 | endOfLine: 'lf', 9 | overrides: [ 10 | { 11 | files: "*.js", 12 | options: { 13 | parser: "flow" 14 | } 15 | } 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTSPtoWEBPlayer 2 | 3 | external video player for projects: 4 | 5 | - [RTSPtoWeb](https://github.com/deepch/RTSPtoWeb) 6 | - [RTSPtoWebRTC](https://github.com/deepch/RTSPtoWebRTC) 7 | - [RTSPtoWSMP4f](https://github.com/deepch/RTSPtoWSMP4f) 8 | - [RTSPtoHLS](https://github.com/deepch/RTSPtoHLS) 9 | - [RTSPtoHLSLL](https://github.com/deepch/RTSPtoHLSLL) 10 | 11 | there is no GUI in this project, you can add your own GUI 12 | 13 | [demo page](http://htmlpreview.github.io/?https://github.com/vdalex25/rtsp-to-web-player/blob/main/dist/index.html) 14 | [publish page](https://vdalex25.github.io/rtsp-to-web-player/dist) 15 | 16 | ## Install 17 | 18 | ```bash 19 | git clone https://github.com/vdalex25/RTSPtoWEBPlayer.git 20 | 21 | cd RTSPtoWEBPlayer 22 | 23 | npm install 24 | 25 | npm run build 26 | ``` 27 | 28 | it's created compiled file `dist/RTSPtoWEBPlayer.js` 29 | 30 | ## Usage 31 | 32 | Add script to your page 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | Create new player 39 | 40 | ```js 41 | const options = { 42 | parentElement: document.getElementById("player"), 43 | }; 44 | const player = new RTSPtoWEBPlayer(options); 45 | player.load( 46 | "ws://localhost:8083/stream/517fe9dbf4b244aaa0330cf582de9932/channel/0/mse?uuid=517fe9dbf4b244aaa0330cf582de9932&channel=0" 47 | ); 48 | ``` 49 | ## Usage multiple players 50 | 51 | Add script to your page 52 | 53 | ```html 54 | 55 | ``` 56 | create containers for players 57 | ```html 58 |
59 |
60 |
61 |
62 | ``` 63 | Create players 64 | 65 | ```js 66 | const players = { 67 | "firstPlayer": new RTSPtoWEBPlayer({parentElement: document.getElementById("player-1")}), 68 | "secondPlayer": new RTSPtoWEBPlayer({parentElement: document.getElementById("player-2")}), 69 | "thirdPlayer": new RTSPtoWEBPlayer({parentElement: document.getElementById("player-3")}), 70 | "fourthPlayer": new RTSPtoWEBPlayer({parentElement: document.getElementById("player-4")}) 71 | } 72 | //play in first and fourth players 73 | players.firstPlayer.load( 74 | "ws://localhost:8083/stream/517fe9dbf4b244aaa0330cf582de9932/channel/0/mse?uuid=517fe9dbf4b244aaa0330cf582de9932&channel=0" 75 | ); 76 | players.fourthPlayer.load( 77 | "ws://localhost:8083/stream/517fe9dbf4b244aaa0330cf582de9932/channel/0/mse?uuid=517fe9dbf4b244aaa0330cf582de9932&channel=0" 78 | ); 79 | ``` 80 | ## Options 81 | 82 | ```js 83 | options = { 84 | parentElement: null, 85 | source: null, 86 | controls: true, 87 | muted: true, 88 | autoplay: true, 89 | loop: false, 90 | hlsjsconfig: {}, 91 | }; 92 | ``` 93 | 94 | #### `parentElement` 95 | 96 | default: null 97 | 98 | HTMLElement 99 | 100 | #### `source` 101 | 102 | link to mediasource. requires explicit protocol http/https or ws/wss 103 | 104 | #### `controls` 105 | 106 | default: true 107 | 108 | show/hide notive video control 109 | 110 | #### `muted` 111 | 112 | default: true 113 | 114 | #### `autoplay` 115 | 116 | default: true 117 | 118 | #### `loop` 119 | 120 | default: false 121 | 122 | #### `hlsjsconfig` 123 | 124 | default: empty; 125 | 126 | full list of config you can see on [API dicumentation hls.js](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) 127 | 128 | #### `webrtcconfig` 129 | 130 | default: 131 | 132 | ```js 133 | { 134 | iceServers: [{ 135 | urls: [ 136 | "stun:stun.l.google.com:19302" 137 | ]} 138 | ], 139 | sdpSemantics: "unified-plan", 140 | bundlePolicy: "max-compat", 141 | iceTransportPolicy: "all"//for option "relay" need use turn server 142 | } 143 | ``` 144 | 145 | full list of config you can see on [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#parameters) 146 | 147 | #### `onWsClose` 148 | default: null; 149 | 150 | handle websocket close event 151 | 152 | example: 153 | ```js 154 | onWsClose: (code, reason)=>{ 155 | switch (code){ 156 | case 1000: 157 | case 1006: 158 | //reconect to socket 159 | player.load(source); 160 | break; 161 | default: 162 | player.destroy(); 163 | } 164 | } 165 | ``` 166 | 167 | ## Methods 168 | 169 | #### `load(source)` 170 | 171 | breaking previos connections and load new source 172 | 173 | ```js 174 | const server = "127.0.0.1:8083"; //server and port where is running one of mediaserver 175 | const uuid = "test"; //stream uuid 176 | const channel = 0; //stream channel optional 177 | 178 | //Project RTSPtoWeb[MSE] 179 | const source = `ws://${server}/stream/${uuid}/channel/${channel}/mse?uuid=${uuid}/&channel=${channel}`; 180 | //Project RTSPtoWeb[WEBRTC] 181 | const source = `http://${server}/stream/${uuid}/channel/${channel}/webrtc?uuid=${uuid}/&channel=${channel}`; 182 | //Project RTSPtoWeb[HLS] 183 | const source = `http://${server}/stream/${uuid}/channel/${channel}/hls/live/index.m3u8`; 184 | //Project RTSPtoWeb[HLSLL] 185 | const source = `http://${server}/stream/${uuid}/channel/${channel}/hlsll/live/index.m3u8`; 186 | 187 | //Project RTSPtoWebRTC[WEBRTC] 188 | const source = `http://${server}/stream/receiver/${uuid}`; 189 | 190 | //Project RTSPtoWSMP4f[MSE] 191 | const source = `ws://${server}/ws/live?suuid=${uuid}`; 192 | 193 | //Project RTSPtoHLS[HLS] 194 | const source = `http://${server}/play/hls/${uuid}/index.m3u8`; 195 | 196 | //Project RTSPtoHLSLL[HLS] 197 | const source = `http://${server}/play/hls/${uuid}/index.m3u8`; 198 | 199 | player.load(source); 200 | ``` 201 | 202 | #### `destroy()` 203 | 204 | breaks all active connections and destroys the player 205 | 206 | #### `control media` 207 | 208 | for player control you can use all methods for video tag [HTMLMediaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement#methods) over player.video 209 | 210 | for example 211 | 212 | ```js 213 | const player = new RTSPtoWEBPlayer({ 214 | parentElement: document.getElementById("player"), 215 | }); 216 | player.load(source_url); 217 | 218 | //pause 219 | player.video.pause(); 220 | //play 221 | player.video.play(); 222 | //get currentTime 223 | console.log(player.video.currentTime); 224 | //set currentTime 225 | player.video.currentTime = 10; 226 | //etc 227 | ``` 228 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RTSPtoWEBPlayer demo 8 | 10 | 11 | 12 | 13 |
14 |

RTSPtoWEBPlayer Example

15 |
16 |
17 |
18 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 77 | 78 | -------------------------------------------------------------------------------- /examples/ReactComponent/README.md: -------------------------------------------------------------------------------- 1 | # React Player Component 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm start 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/ReactComponent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtsp-to-web-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "cra-template": "1.1.3", 7 | "react": "^18.0.0", 8 | "react-dom": "^18.0.0", 9 | "react-scripts": "^5.0.0", 10 | "rtsptowebplayer": "github:vdalex25/rtsp-to-web-player#51832916615d9e6c72ae2962ebec35a5061eb775" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build" 15 | }, 16 | "eslintConfig": { 17 | "extends": [ 18 | "react-app", 19 | "react-app/jest" 20 | ] 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/ReactComponent/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RTSPtoWEBPlayerReact 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/ReactComponent/src/App.js: -------------------------------------------------------------------------------- 1 | import ReactPlayer from "./components/react-player/react-player"; 2 | import {useState} from "react"; 3 | import InputGroup from "./components/input-group"; 4 | 5 | const App = () => { 6 | 7 | const [url, setUrl] = useState(null); 8 | 9 | return (
10 |

Simple Example RTSPtoWEB player React

11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | PLAYER 19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 |
) 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /examples/ReactComponent/src/components/control-btn.js: -------------------------------------------------------------------------------- 1 | import {ReactComponent as PlayImg} from '../components/images/play.svg'; 2 | import {ReactComponent as PauseImg} from '../components/images/pause.svg'; 3 | 4 | const ControlButton = ({type, onClick}) => { 5 | switch (type) { 6 | case 'pause': 7 | return () 8 | case 'play': 9 | return () 10 | default: 11 | return null; 12 | } 13 | } 14 | 15 | export default ControlButton; -------------------------------------------------------------------------------- /examples/ReactComponent/src/components/images/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/ReactComponent/src/components/images/play.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/ReactComponent/src/components/input-group.js: -------------------------------------------------------------------------------- 1 | import {useState} from "react"; 2 | 3 | const InputGroup = ({setUrl}) => { 4 | 5 | const [linkInput, setLinkInput] = useState(''); 6 | const onClickHandler = () => { 7 | try { 8 | new URL(linkInput); 9 | } catch (e) { 10 | console.warn("URL is not valid ") 11 | return false; 12 | } 13 | setUrl(linkInput); 14 | } 15 | 16 | return (
17 | { 22 | setLinkInput(e.target.value); 23 | }}/> 24 | 25 |
) 26 | } 27 | 28 | export default InputGroup; -------------------------------------------------------------------------------- /examples/ReactComponent/src/components/react-player/react-player.css: -------------------------------------------------------------------------------- 1 | .player-wrapper { 2 | position: relative; 3 | aspectRatio: '16/9'; 4 | } 5 | 6 | .player-wrapper video { 7 | margin-bottom: -6px; 8 | } 9 | 10 | .player-wrapper .control { 11 | position: absolute; 12 | bottom: 0; 13 | width: 100%; 14 | height: 50px; 15 | padding: 10px; 16 | } 17 | 18 | .player-wrapper .control span { 19 | width: 40px; 20 | height: 40px; 21 | cursor: pointer; 22 | opacity: 0.7; 23 | } 24 | 25 | 26 | .player-wrapper .control span:hover { 27 | opacity: 1; 28 | } 29 | 30 | .player-wrapper .control span svg { 31 | width: 100%; 32 | height: 100%; 33 | } -------------------------------------------------------------------------------- /examples/ReactComponent/src/components/react-player/react-player.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef, useState} from "react"; 2 | import './react-player.css'; 3 | import RTSPtoWEBPlayer from "rtsptowebplayer"; 4 | import ControlButton from "../control-btn"; 5 | 6 | const ReactPlayer = ({url}) => { 7 | 8 | const playerElement = useRef(null); 9 | const [player, setPlayer] = useState(null); 10 | const [state, setState] = useState('pause'); 11 | const stateListener = (e) => { 12 | setState(e.type); 13 | } 14 | const playPause = () => { 15 | if (player.video.src !== '') { 16 | player.video.paused ? player.video.play() : player.video.pause(); 17 | } 18 | } 19 | 20 | useEffect(() => { 21 | if (player === null) { 22 | setPlayer(new RTSPtoWEBPlayer({ 23 | parentElement: playerElement.current, controls: false 24 | })); 25 | } else if (player.video.onpause === null && player.video.onplay === null) { 26 | player.video.onpause = stateListener; 27 | player.video.onplay = stateListener; 28 | } 29 | if (url !== null && player !== null) { 30 | player.load(url); 31 | } 32 | 33 | return () => { 34 | if (player !== null) { 35 | player.destroy(); 36 | } 37 | } 38 | }, [url, player]); 39 | 40 | return (
41 |
42 |
43 | 44 |
45 |
) 46 | } 47 | 48 | export default ReactPlayer; -------------------------------------------------------------------------------- /examples/ReactComponent/src/index.js: -------------------------------------------------------------------------------- 1 | import {createRoot} from "react-dom/client"; 2 | import App from "./App"; 3 | 4 | const container = document.getElementById('root'); 5 | const root = createRoot(container); 6 | 7 | root.render(); -------------------------------------------------------------------------------- /examples/Vue3Component/README.md: -------------------------------------------------------------------------------- 1 | # Vue Player Component 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/Vue3Component/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples/Vue3Component/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/Vue3Component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RTSPtoWEBPlayerVue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.8.3", 12 | "vue": "^3.2.13" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.12.16", 16 | "@babel/eslint-parser": "^7.12.16", 17 | "@vue/cli-plugin-babel": "~5.0.0", 18 | "@vue/cli-plugin-eslint": "~5.0.0", 19 | "@vue/cli-service": "~5.0.0", 20 | "eslint": "^7.32.0", 21 | "eslint-plugin-vue": "^8.0.3", 22 | "rtsptowebplayer": "github:vdalex25/rtsp-to-web-player#v1.0.5" 23 | }, 24 | "eslintConfig": { 25 | "root": true, 26 | "env": { 27 | "node": true 28 | }, 29 | "extends": [ 30 | "plugin:vue/vue3-essential", 31 | "eslint:recommended" 32 | ], 33 | "parserOptions": { 34 | "parser": "@babel/eslint-parser" 35 | }, 36 | "rules": {} 37 | }, 38 | "browserslist": [ 39 | "> 1%", 40 | "last 2 versions", 41 | "not dead", 42 | "not ie 11" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/Vue3Component/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/Vue3Component/src/App.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /examples/Vue3Component/src/components/PlayerVue.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /examples/Vue3Component/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/Vue3Component/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | transpileDependencies: true 4 | }) 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RTSPtoWEBPlayer demo 8 | 10 | 11 | 12 | 13 |
14 |

RTSPtoWEBPlayer Example

15 |
16 |
17 |
18 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 61 | 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import RTSPtoWEBPlayer from "./src/rtsp-to-web-player"; 2 | export default RTSPtoWEBPlayer; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtsptowebplayer", 3 | "version": "1.0.4", 4 | "description": "js player for RTSPtoWEB", 5 | "main": "./src/RTSPtoWEBPlayer.js", 6 | "scripts": { 7 | "build": "webpack --progress --color", 8 | "serve": "webpack-dev-server --progress --color", 9 | "prettier": "npx prettier --write \"src/*.*\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vdalex25/rtsp-to-web-player.git" 14 | }, 15 | "author": "vdalex", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/vdalex25/rtsp-to-web-player/issues" 19 | }, 20 | "homepage": "https://github.com/vdalex25/rtsp-to-web-player#readme", 21 | "devDependencies": { 22 | "@babel/cli": "^7.23.9", 23 | "@babel/core": "^7.23.9", 24 | "@babel/plugin-proposal-class-properties": "^7.18.6", 25 | "@babel/plugin-transform-class-properties": "^7.23.3", 26 | "@babel/preset-env": "^7.23.9", 27 | "babel-loader": "^9.1.3", 28 | "babel-plugin-lodash": "^3.3.2", 29 | "css-loader": "^6.10.0", 30 | "eslint": "^8.56.0", 31 | "eslint-config-google": "^0.14.0", 32 | "eslint-plugin-react": "^7.33.2", 33 | "prettier": "^3.2.5", 34 | "style-loader": "^3.3.4", 35 | "webpack": "^5.90.1", 36 | "webpack-cli": "^5.1.4", 37 | "webpack-dev-server": "^5.0.0" 38 | }, 39 | "dependencies": { 40 | "@babel/eslint-parser": "^7.23.10", 41 | "ajv": "^8.12.0", 42 | "hls.js": "^1.5.4", 43 | "webrtc-adapter": "^8.2.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/rtsp-to-web-player.css: -------------------------------------------------------------------------------- 1 | .RTSPtoWEBPlayer { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .RTSPtoWEBPlayer video { 7 | width: 100%; 8 | height: 100%; 9 | background: black; 10 | } 11 | -------------------------------------------------------------------------------- /src/rtsp-to-web-player.js: -------------------------------------------------------------------------------- 1 | import 'webrtc-adapter'; 2 | import Hls from 'hls.js/dist/hls.light.min.js'; 3 | import './rtsp-to-web-player.css'; 4 | 5 | export default class RTSPtoWEBPlayer { 6 | MSE = null; 7 | MSEStreamingStarted = false; 8 | MSESourceBuffer = null; 9 | turn = []; 10 | codec = null; 11 | webSocket = null; 12 | webrtc = null; 13 | webRtcSocket = null; 14 | currentPlayerType = null; 15 | hidden = 'hidden'; 16 | paused = false; 17 | presets = null; 18 | audio_tracks = null; 19 | switchFlag = false; 20 | user_state = { 21 | paused: false, 22 | }; 23 | options = { 24 | parentElement: null, 25 | source: null, 26 | controls: true, 27 | muted: true, 28 | autoplay: true, 29 | loop: false, 30 | hlsjsconfig: {}, 31 | webrtcconfig: { 32 | iceServers: [ 33 | { 34 | urls: ['stun:stun.l.google.com:19302'], 35 | }, 36 | ], 37 | sdpSemantics: 'unified-plan', 38 | bundlePolicy: 'max-compat', 39 | //iceTransportPolicy: "relay", 40 | // for option "relay" need use turn server 41 | }, 42 | debug: false, 43 | getPresets: null, 44 | onResolutionChange: null, 45 | latency: null, 46 | onWsClose: null, 47 | }; 48 | 49 | constructor(options) { 50 | this.options = {...this.options, ...options}; 51 | this.createElements(); 52 | if (this.options.parentElement) { 53 | this.attachTo(this.options.parentElement); 54 | } 55 | this.defDocumentHidden(); 56 | } 57 | 58 | createElements = () => { 59 | //video 60 | this.video = document.createElement('video'); 61 | this.video.setAttribute('playsinline', ''); 62 | this.video.muted = this.options.muted ? true : false; 63 | this.video.controls = this.options.controls ? true : false; 64 | this.video.autoplay = this.options.autoplay ? true : false; 65 | this.video.loop = this.options.loop ? true : false; 66 | 67 | this.addVideoListeners(); 68 | //wrapper 69 | this.player = document.createElement('div'); 70 | this.player.classList.add('RTSPtoWEBPlayer'); 71 | this.player.append(this.video); 72 | }; 73 | 74 | attachTo = element => { 75 | this.options.parentElement = element; 76 | this.options.parentElement.innerHTML = ''; 77 | this.options.parentElement.append(this.player); 78 | if (this.options.source) { 79 | this.load(this.options.source); 80 | } 81 | }; 82 | 83 | load = source => { 84 | this.options.source = source; 85 | this.destroy(); 86 | const sourceType = new URL(this.options.source); 87 | if (sourceType.protocol === 'http:' || sourceType.protocol === 'https:') { 88 | if (this.options.source.indexOf('m3u8') !== -1) { 89 | this.currentPlayerType = 'hls'; 90 | this.hlsPlayer(); 91 | } else if (this.options.source.indexOf('.mp4') !== -1) { 92 | this.currentPlayerType = 'mp4'; 93 | this.mp4Player(); 94 | } else { 95 | this.currentPlayerType = 'rtc'; 96 | this.webRtcPlayer(); 97 | } 98 | } else if (sourceType.protocol === 'ws:' || sourceType.protocol === 'wss:') { 99 | if (this.options.source.indexOf('webrtc') !== -1) { 100 | this.currentPlayerType = 'ws-rtc'; 101 | this.webRtcOverSocket(); 102 | } else if ( 103 | this.options.source.indexOf('on-air') !== -1 || 104 | this.options.source.indexOf('preview') !== -1 105 | ) { 106 | this.currentPlayerType = 'ws-new'; 107 | this.newMsePlayer(); 108 | } else { 109 | this.currentPlayerType = 'ws'; 110 | this.msePlayer(); 111 | } 112 | } else { 113 | this.currentPlayerType = null; 114 | } 115 | }; 116 | 117 | newMsePlayer = () => { 118 | this.webSocket = new WebSocket(this.options.source); 119 | 120 | this.webSocket.onclose = e => { 121 | if (typeof this.options.onWsClose === 'function') { 122 | this.options.onWsClose(e.code, e.reason); 123 | } 124 | this.debugLogger(e); 125 | }; 126 | 127 | this.webSocket.onmessage = ({data}) => { 128 | this.messageHandlerMSE(data); 129 | }; 130 | }; 131 | 132 | webRtcOverSocket = () => { 133 | this.webRtcSocket = new WebSocket(this.options.source); 134 | this.webRtcSocket.onopen = () => { 135 | this.webRtcPlayer(); 136 | }; 137 | this.webRtcSocket.onclose = e => { 138 | this.debugLogger(e); 139 | this.webRtcSocket.onmessage = null; 140 | if (typeof this.options.onWsClose === 'function') { 141 | this.options.onWsClose(e.code, e.reason); 142 | } 143 | }; 144 | this.webRtcSocket.onerror = e => { 145 | this.debugLogger(e); 146 | }; 147 | this.webRtcSocket.onmessage = ({data}) => { 148 | this.webRtcSocketMessageHandler(data); 149 | }; 150 | }; 151 | 152 | webRtcSocketMessageHandler = data => { 153 | data = JSON.parse(data); 154 | switch (data.method) { 155 | case 'meta_response': 156 | this.presets = data.payload.streams; 157 | if (typeof this.options.getPresets === 'function') { 158 | this.options.getPresets(this.presets, data.payload.audio_tracks); 159 | } 160 | const arr_video = data.payload.streams.filter(item => { 161 | return item.default; 162 | }); 163 | const arr_audio = data.payload.audio_tracks.filter(item => { 164 | return item.default; 165 | }); 166 | const default_video = arr_video.length > 0 ? arr_video[0].idx : data.payload.streams[0].idx; 167 | const default_audio = 168 | arr_audio.length > 0 ? arr_audio[0].idx : data.payload.audio_tracks[0].idx; 169 | 170 | this.user_state.video_track_id = default_video; 171 | this.user_state.audio_track_id = default_audio; 172 | //создаем локальный оффер 173 | this.webRtcSocketOffer(); 174 | break; 175 | case 'offer': 176 | this.webrtc.setRemoteDescription(new RTCSessionDescription(data.payload)); 177 | break; 178 | case 'ice_candidate': 179 | this.webrtc.addIceCandidate(data.payload); 180 | break; 181 | case 'answer': 182 | this.webrtc.setRemoteDescription(new RTCSessionDescription(data.payload)); 183 | break; 184 | default: 185 | console.warn('unsupported method', data.method); 186 | return; 187 | } 188 | }; 189 | 190 | webRtcSocketOffer = async () => { 191 | const offer = await this.webrtc.createOffer({ 192 | offerToReceiveAudio: true, 193 | offerToReceiveVideo: true, 194 | }); 195 | await this.webrtc.setLocalDescription(offer); 196 | }; 197 | 198 | messageHandlerMSE = data => { 199 | if (typeof data === 'string') { 200 | try { 201 | data = JSON.parse(data); 202 | switch (data.method) { 203 | case 'play_response': 204 | break; 205 | case 'meta_response': 206 | this.presets = data.payload.streams; 207 | if (typeof this.options.getPresets === 'function') { 208 | this.options.getPresets(this.presets, data.payload.audio_tracks); 209 | } 210 | const arr_video = data.payload.streams.filter(item => { 211 | return item.default; 212 | }); 213 | 214 | const arr_audio = data.payload.audio_tracks.filter(item => { 215 | return item.default; 216 | }); 217 | const default_video = 218 | arr_video.length > 0 ? arr_video[0].idx : data.payload.streams[0].idx; 219 | const default_audio = 220 | arr_audio.length > 0 ? arr_audio[0].idx : data.payload.audio_tracks[0].idx; 221 | 222 | this.user_state.video_track_id = default_video; 223 | this.user_state.audio_track_id = default_audio; 224 | 225 | this.playPresetMSE(default_video, default_audio); 226 | break; 227 | default: 228 | console.log(data.method); 229 | return; 230 | } 231 | 232 | if (data.method === 'play_response') { 233 | } else { 234 | } 235 | } catch (e) { 236 | this.debugLogger(e); 237 | } 238 | } else if (typeof data === 'object') { 239 | data.arrayBuffer().then(packet => { 240 | this.readPacket(packet); 241 | }); 242 | // 243 | } 244 | }; 245 | 246 | getPresets = () => { 247 | return this.presets; 248 | }; 249 | 250 | playPresetMSE = (videoIdx, audioIdx) => { 251 | this.codec = this.presets.filter(item => item.idx === videoIdx)[0].codecs; 252 | const answer = JSON.stringify({ 253 | method: 'user_state', 254 | payload: this.user_state, 255 | }); 256 | this.MSE = new MediaSource(); 257 | this.video.src = window.URL.createObjectURL(this.MSE); 258 | this.MSE.addEventListener('sourceopen', () => { 259 | this.MSESourceBuffer = this.MSE.addSourceBuffer(`video/mp4; codecs="${this.codec}"`); 260 | this.MSESourceBuffer.mode = 'segments'; 261 | this.MSESourceBuffer.addEventListener('updateend', this.pushPacket); 262 | this.webSocket.send(answer); 263 | }); 264 | }; 265 | 266 | switchStream = index => { 267 | this.codec = this.presets[index].codecs; 268 | this.user_state.video_track_id = this.presets[index].idx; 269 | if (this.webRtcSocket) { 270 | this.webRtcSocket.send( 271 | JSON.stringify({ 272 | method: 'user_state', 273 | payload: this.user_state, 274 | }), 275 | ); 276 | } else { 277 | this.switchFlag = true; 278 | this.webSocket.send( 279 | JSON.stringify({ 280 | method: 'user_state', 281 | payload: this.user_state, 282 | }), 283 | ); 284 | this.MSESourceBuffer.timestampOffset = this.MSESourceBuffer.appendWindowStart = 285 | this.MSESourceBuffer.buffered.end(this.MSESourceBuffer.buffered.length - 1); 286 | } 287 | }; 288 | 289 | switchAudio = idx => { 290 | this.user_state.audio_track_id = idx; 291 | if (this.webRtcSocket) { 292 | this.webRtcSocket.send( 293 | JSON.stringify({ 294 | method: 'user_state', 295 | payload: this.user_state, 296 | }), 297 | ); 298 | } else { 299 | this.webSocket.send( 300 | JSON.stringify({ 301 | method: 'set_audio_track', 302 | payload: { 303 | audio_track_id: idx, 304 | }, 305 | }), 306 | ); 307 | this.video.currentTime += 0.01; 308 | } 309 | }; 310 | 311 | addMseListeners = () => { 312 | this.MSE.addEventListener('sourceopen', this.sourceOpenHandler); 313 | }; 314 | 315 | sourceOpenHandler = () => { 316 | this.websocketEvents(); 317 | }; 318 | 319 | websocketEvents = () => { 320 | this.webSocket = new WebSocket(this.options.source); 321 | this.webSocket.binaryType = 'arraybuffer'; 322 | this.webSocket.onclose = e => { 323 | this.webSocket.onmessage = null; 324 | if (typeof this.options.onWsClose === 'function') { 325 | this.options.onWsClose(e.code, e.reason); 326 | } 327 | }; 328 | this.webSocket.onmessage = ({data}) => { 329 | if (typeof data === 'object') { 330 | if (this.codec === null) { 331 | this.codec = new TextDecoder('utf-8').decode(new Uint8Array(data).slice(1)); 332 | const mimeCodec = 'video/mp4; codecs="' + this.codec + '"'; 333 | if(!this.mseIsTypeSupported(mimeCodec)){ 334 | console.warn('No decoders for requested formats: '+mimeCodec); 335 | this.webSocket.close(1000); 336 | return; 337 | } 338 | this.MSESourceBuffer = this.MSE.addSourceBuffer(mimeCodec); 339 | this.MSESourceBuffer.mode = 'segments'; 340 | this.MSE.duration = Infinity; 341 | this.MSESourceBuffer.addEventListener('updateend', this.pushPacket); 342 | } else { 343 | if (!this.paused) { 344 | this.readPacket(data); 345 | } 346 | } 347 | } else { 348 | if (this.codec !== null) { 349 | //console.log(data); 350 | } else { 351 | this.codec = data; 352 | this.MSESourceBuffer = this.MSE.addSourceBuffer(`video/mp4; codecs="${this.codec}"`); 353 | this.MSESourceBuffer.mode = 'segments'; 354 | this.MSE.duration = Infinity; 355 | this.MSESourceBuffer.addEventListener('updateend', this.pushPacket); 356 | } 357 | } 358 | 359 | if (document[this.hidden] && this.video.buffered.length) { 360 | this.video.currentTime = this.video.buffered.end(this.video.buffered.length - 1) - 1; 361 | } 362 | }; 363 | }; 364 | 365 | readPacket = packet => { 366 | if (this.video.buffered && this.video.currentTime > 0) { 367 | if (typeof this.options.latency === 'function') { 368 | this.options.latency( 369 | this.video.buffered.length, 370 | this.video.buffered.end(this.video.buffered.length - 1), 371 | this.video.currentTime, 372 | ); 373 | } 374 | 375 | if (this.video.currentTime < this.video.buffered.start(this.video.buffered.length - 1)) { 376 | this.video.currentTime = this.video.buffered.end(this.video.buffered.length - 1); 377 | } 378 | } 379 | if (!this.MSEStreamingStarted) { 380 | try { 381 | this.MSESourceBuffer.appendBuffer(packet); 382 | this.MSEStreamingStarted = true; 383 | } catch (e) { 384 | this.debugLogger(e); 385 | } 386 | return; 387 | } 388 | this.turn.push(packet); 389 | this.pushPacket(); 390 | }; 391 | 392 | pushPacket = () => { 393 | if (!this.MSESourceBuffer.updating) { 394 | if (this.turn.length > 0) { 395 | const packet = this.turn.shift(); 396 | try { 397 | this.MSESourceBuffer.appendBuffer(packet); 398 | } catch (err) { 399 | this.debugLogger(err); 400 | } 401 | } else { 402 | this.MSEStreamingStarted = false; 403 | } 404 | } 405 | }; 406 | 407 | mp4Player = () => { 408 | this.video.src = this.options.source; 409 | }; 410 | 411 | msePlayer = () => { 412 | this.MSE = this.getMediaSource(); 413 | this.video.src = window.URL.createObjectURL(this.MSE); 414 | this.addMseListeners(); 415 | }; 416 | 417 | hlsPlayer = () => { 418 | if (this.video.canPlayType('application/vnd.apple.mpegurl')) { 419 | this.video.src = this.options.source; 420 | } else if (Hls.isSupported()) { 421 | this.hls = new Hls(this.options.hlsjsconfig); 422 | this.hls.on(Hls.Events.ERROR, function (event, data) { 423 | if(data?.error?.name === 'NotSupportedError'){ 424 | console.warn('No decoders for requested formats: '+data?.mimeType); 425 | } 426 | if(data.details==='levelEmptyError'){ 427 | console.warn(data.reason); 428 | } 429 | }) 430 | this.hls.loadSource(this.options.source); 431 | this.hls.attachMedia(this.video); 432 | } else { 433 | console.warn('UNSUPPOERED MEDIA SOURCE'); 434 | } 435 | }; 436 | 437 | webRtcPlayer = async () => { 438 | this.mediaStream = new MediaStream(); 439 | this.video.srcObject = this.mediaStream; 440 | this.webrtc = new RTCPeerConnection(this.options.webrtcconfig); 441 | this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded; 442 | this.webrtc.onsignalingstatechange = this.signalingstatechange; 443 | this.webrtc.onicegatheringstatechange = this.icegatheringstatechange; 444 | this.webrtc.onicecandidate = this.icecandidate; 445 | this.webrtc.onicecandidateerror = this.icecandidateerror; 446 | this.webrtc.onconnectionstatechange = this.connectionstatechange; 447 | this.webrtc.oniceconnectionstatechange = this.iceconnectionstatechange; 448 | this.webrtc.ontrack = this.onTrack; 449 | if (!this.webRtcSocket) { 450 | /* 451 | * for older schema initiate connection create local description 452 | */ 453 | const offer = await this.webrtc.createOffer({ 454 | offerToReceiveAudio: false, 455 | offerToReceiveVideo: true, 456 | }); 457 | await this.webrtc.setLocalDescription(offer); 458 | } 459 | }; 460 | 461 | getMediaSource = () => { 462 | if (window.ManagedMediaSource) { 463 | this.video.disableRemotePlayback = true; 464 | return new window.ManagedMediaSource(); 465 | } 466 | if (window.MediaSource) { 467 | return new window.MediaSource(); 468 | } 469 | } 470 | 471 | mseIsTypeSupported = function(mimeCodec) { 472 | return ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec))||('ManagedMediaSource' in window && window.ManagedMediaSource.isTypeSupported(mimeCodec)) 473 | } 474 | 475 | handleNegotiationNeeded = async e => { 476 | /* 477 | * in this project this handler is not needed, but in another it can be useful 478 | */ 479 | this.debugLogger('handleNegotiationNeeded'); 480 | if (this.webRtcSocket) { 481 | const offer = await this.webrtc.createOffer({ 482 | offerToReceiveAudio: false, 483 | offerToReceiveVideo: true, 484 | }); 485 | await this.webrtc.setLocalDescription(offer); 486 | } 487 | }; 488 | 489 | signalingstatechange = async () => { 490 | switch (this.webrtc.signalingState) { 491 | case 'have-remote-offer': 492 | this.debugLogger('this.webrtc.signalingState====>[have-remote-offer]'); 493 | if (this.webRtcSocket) { 494 | const answer = await this.webrtc.createAnswer(); 495 | await this.webrtc.setLocalDescription(answer); 496 | 497 | this.webRtcSocket.send( 498 | JSON.stringify({ 499 | method: 'answer', 500 | payload: { 501 | sdp: answer, 502 | user_state: this.user_state, 503 | }, 504 | }), 505 | ); 506 | } 507 | break; 508 | case 'have-local-offer': 509 | this.debugLogger('this.webrtc.signalingState====>[have-local-offer]'); 510 | if (!this.webRtcSocket) { 511 | const suuid = new URL(this.options.source).pathname.split('/').slice(-1); 512 | const formData = new FormData(); 513 | formData.append('data', btoa(this.webrtc.localDescription.sdp)); 514 | formData.append('suuid', suuid); 515 | const response = await fetch(this.options.source, { 516 | method: 'POST', 517 | body: formData, 518 | }); 519 | if (response.ok) { 520 | const remoteDescription = await response.text(); 521 | this.webrtc.setRemoteDescription( 522 | new RTCSessionDescription({ 523 | type: 'answer', 524 | sdp: atob(remoteDescription), 525 | }), 526 | ); 527 | } 528 | } else { 529 | this.webRtcSocket.send( 530 | JSON.stringify({ 531 | method: 'offer', 532 | payload: { 533 | user_state: this.user_state, 534 | sdp: this.webrtc.localDescription, 535 | }, 536 | }), 537 | ); 538 | } 539 | 540 | break; 541 | case 'stable': 542 | /* 543 | * There is no ongoing exchange of offer and answer underway. 544 | * This may mean that the RTCPeerConnection object is new, in which case both the localDescription and remoteDescription are null; 545 | * it may also mean that negotiation is complete and a connection has been established. 546 | */ 547 | this.debugLogger('this.webrtc.signalingState====>[stable]'); 548 | break; 549 | 550 | case 'closed': 551 | /* 552 | * The RTCPeerConnection has been closed. 553 | */ 554 | this.debugLogger('this.webrtc.signalingState====>[closed]'); 555 | this.destroy(); 556 | break; 557 | 558 | default: 559 | console.log(`unhandled signalingState is ${this.webrtc.signalingState}`); 560 | break; 561 | } 562 | }; 563 | 564 | icegatheringstatechange = () => { 565 | switch (this.webrtc.iceGatheringState) { 566 | case 'gathering': 567 | /* collection of candidates has begun */ 568 | this.debugLogger('collection of candidates has begun'); 569 | break; 570 | case 'complete': 571 | /* collection of candidates is finished */ 572 | this.debugLogger('collection of candidates is finished'); 573 | break; 574 | } 575 | }; 576 | 577 | icecandidate = event => { 578 | this.debugLogger('icecandidate\n', event); 579 | if (this.webRtcSocket) { 580 | if (event.candidate && event.candidate.candidate !== '') { 581 | this.webRtcSocket.send( 582 | JSON.stringify({ 583 | method: 'ice_candidate', 584 | payload: event.candidate, 585 | }), 586 | ); 587 | } 588 | } 589 | }; 590 | 591 | icecandidateerror = event => { 592 | this.debugLogger( 593 | 'icecandidateerror\n', 594 | `hostCandidate: ${event.hostCandidate} CODE: ${event.errorCode} TEXT: ${event.errorText}`, 595 | ); 596 | }; 597 | 598 | connectionstatechange = e => { 599 | switch (this.webrtc.connectionState) { 600 | case 'new': 601 | case 'connected': 602 | this.debugLogger('connected'); 603 | break; 604 | case 'disconnected': 605 | this.debugLogger('disconnected...'); 606 | break; 607 | case 'closed': 608 | this.debugLogger('Offline'); 609 | break; 610 | case 'failed': 611 | this.webrtc.restartIce(); 612 | this.debugLogger('Error'); 613 | break; 614 | default: 615 | this.debugLogger(`Unhadled state: ${this.webrtc.connectionState}`); 616 | break; 617 | } 618 | }; 619 | iceconnectionstatechange = () => { 620 | this.debugLogger('iceconnectionstatechange\n', this.webrtc.iceConnectionState); 621 | }; 622 | 623 | onTrack = event => { 624 | this.debugLogger('onTrack\n'); 625 | //make sure there is only one video track in mediaStream 626 | if (event.track.kind === 'video' && this.mediaStream.getVideoTracks().length > 0) { 627 | this.mediaStream.removeTrack(this.mediaStream.getVideoTracks()[0]); 628 | } 629 | if (event.track.kind === 'audio' && this.mediaStream.getAudioTracks().length > 0) { 630 | this.mediaStream.removeTrack(this.mediaStream.getAudioTracks()[0]); 631 | } 632 | this.mediaStream.addTrack(event.track); 633 | }; 634 | 635 | destroy = () => { 636 | this.codec = null; 637 | this.presets = null; 638 | this.audio_tracks = null; 639 | if (this.currentPlayerType != null) { 640 | switch (this.currentPlayerType) { 641 | case 'hls': 642 | if (this.hls != null) { 643 | this.hls.destroy(); 644 | } 645 | break; 646 | 647 | case 'rtc': 648 | if (this.webrtc != null) { 649 | this.webrtc.close(); 650 | this.webrtc = null; 651 | this.video.srcObject = null; 652 | this.mediaStream = null; 653 | } 654 | break; 655 | 656 | case 'ws': 657 | case 'ws-new': 658 | this.webSocket.onerror = null; 659 | this.webSocket.onopen = null; 660 | this.webSocket.onmessage = null; 661 | this.webSocket.onclose = null; 662 | this.webSocket.close(1000); 663 | this.turn = []; 664 | break; 665 | case 'ws-rtc': 666 | this.webRtcSocket.onerror = null; 667 | this.webRtcSocket.onopen = null; 668 | this.webRtcSocket.onmessage = null; 669 | this.webRtcSocket.onclose = null; 670 | this.webRtcSocket.close(1000); 671 | this.turn = []; 672 | if (this.webrtc != null) { 673 | this.webrtc.close(); 674 | this.webrtc = null; 675 | this.video.srcObject = null; 676 | this.mediaStream = null; 677 | } 678 | break; 679 | default: 680 | } 681 | this.video.pause(); 682 | this.video.removeAttribute('src'); // empty source 683 | this.video.load(); 684 | } 685 | }; 686 | 687 | addVideoListeners = () => { 688 | this.video.addEventListener('error', e => { 689 | this.debugLogger('[ video listener ]', e); 690 | this.destroy(); 691 | }); 692 | 693 | this.video.addEventListener('play', () => { 694 | this.paused = false; 695 | }); 696 | 697 | this.video.addEventListener('pause', () => { 698 | this.paused = true; 699 | }); 700 | 701 | this.video.addEventListener('resize', () => { 702 | if (typeof this.options.onResolutionChange === 'function') { 703 | this.options.onResolutionChange(this.video.videoWidth, this.video.videoHeight); 704 | } 705 | }); 706 | 707 | this.video.addEventListener('progress', () => { 708 | if (this.currentPlayerType === 'ws' && this.video.buffered.length > 0) { 709 | if (this.video.currentTime < this.video.buffered.start(this.video.buffered.length - 1)) { 710 | this.video.currentTime = this.video.buffered.end(this.video.buffered.length - 1) - 1; 711 | } 712 | } 713 | }); 714 | 715 | this.video.addEventListener('canplay', () => { 716 | if (this.currentPlayerType === 'ws') { 717 | if (this.video.paused && this.video.autoplay) { 718 | this.video.play(); 719 | } 720 | } 721 | }); 722 | }; 723 | 724 | defDocumentHidden() { 725 | if (typeof document.hidden !== 'undefined') { 726 | this.hidden = 'hidden'; 727 | } else if (typeof document.msHidden !== 'undefined') { 728 | this.hidden = 'msHidden'; 729 | } else if (typeof document.webkitHidden !== 'undefined') { 730 | this.hidden = 'webkitHidden'; 731 | } 732 | } 733 | 734 | getImageBase64 = () => { 735 | const canvas = document.createElement('canvas'); 736 | canvas.width = this.video.videoWidth; 737 | canvas.height = this.video.videoHeight; 738 | canvas.getContext('2d').drawImage(this.video, 0, 0, canvas.width, canvas.height); 739 | const dataURL = canvas.toDataURL(); 740 | canvas.remove(); 741 | return dataURL; 742 | }; 743 | 744 | debugLogger = (...arg) => { 745 | if (this.options.debug) { 746 | if (this.options.debug === 'trace') { 747 | console.trace(...arg); 748 | } else { 749 | const d = new Date(); 750 | console.log(d.toLocaleTimeString() + `.${d.getMilliseconds()}`, ...arg); 751 | } 752 | } 753 | }; 754 | } 755 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", //production,development 5 | watch: false, 6 | target: "web", 7 | entry: { 8 | RTSPtoWEBPlayer: "./src/rtsp-to-web-player.js", 9 | }, 10 | output: { 11 | path: __dirname + "/dist", 12 | filename: "[name].js", 13 | library: "[name]", 14 | libraryExport: "default", 15 | globalObject: "this", 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | loader: "babel-loader", 22 | options: { 23 | presets: [ 24 | [ 25 | "@babel/preset-env", 26 | { 27 | targets: { 28 | safari: "11", 29 | }, 30 | }, 31 | ], 32 | ], 33 | plugins: ["@babel/plugin-proposal-class-properties"], 34 | }, 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: ["style-loader", "css-loader"], 39 | }, 40 | ], 41 | }, 42 | devServer: { 43 | static: { 44 | directory: path.join(__dirname, "dist"), 45 | }, 46 | compress: true, 47 | port: 9000, 48 | }, 49 | }; 50 | --------------------------------------------------------------------------------