├── .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 |
41 |
)
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 |
2 |
3 |
Simple Example RTSPtoWEB mediaplayer VUE.JS
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
44 |
--------------------------------------------------------------------------------
/examples/Vue3Component/src/components/PlayerVue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
--------------------------------------------------------------------------------